Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: add support for oauth #1045

Closed
wants to merge 10 commits into from
15 changes: 15 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
twilio-node changelog
=====================

[2024-11-15] Version 5.3.6
--------------------------
**Library - Chore**
- [PR #1040](https://github.com/twilio/twilio-node/pull/1040): pass http agent options to client. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)!
- [PR #1041](https://github.com/twilio/twilio-node/pull/1041): remove preview sync api. Thanks to [@sbansla](https://github.com/sbansla)!

**Api**
- Added `ivr-virtual-agent-custom-voices` and `ivr-virtual-agent-genai` to `usage_record` API.
- Add open-api file tag to realtime_transcriptions

**Taskrouter**
- Add `api-tag` property to workers reservation
- Add `api-tag` property to task reservation


[2024-10-24] Version 5.3.5
--------------------------
**Conversations**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "twilio",
"description": "A Twilio helper library",
"version": "5.3.5",
"version": "5.3.6",
"author": "API Team <[email protected]>",
"contributors": [
{
Expand Down
8 changes: 8 additions & 0 deletions src/auth_strategy/AuthStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default abstract class AuthStrategy {
private authType: string;
protected constructor(authType: string) {
this.authType = authType;
}
abstract getAuthString(): Promise<string>;
abstract requiresAuthentication(): boolean;
}
23 changes: 23 additions & 0 deletions src/auth_strategy/BasicAuthStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AuthStrategy from "./AuthStrategy";

export default class BasicAuthStrategy extends AuthStrategy {
private username: string;
private password: string;

constructor(username: string, password: string) {
super("basic");
this.username = username;
this.password = password;
}

getAuthString(): Promise<string> {
const auth = Buffer.from(this.username + ":" + this.password).toString(
"base64"
);
return Promise.resolve(`Basic ${auth}`);
}

requiresAuthentication(): boolean {
return true;
}
}
15 changes: 15 additions & 0 deletions src/auth_strategy/NoAuthStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import AuthStrategy from "./AuthStrategy";

export default class NoAuthStrategy extends AuthStrategy {
constructor() {
super("noauth");
}

getAuthString(): Promise<string> {
return Promise.resolve("");
}

requiresAuthentication(): boolean {
return false;
}
}
67 changes: 67 additions & 0 deletions src/auth_strategy/TokenAuthStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import AuthStrategy from "./AuthStrategy";
import TokenManager from "../http/bearer_token/TokenManager";
import jwt, { JwtPayload } from "jsonwebtoken";

export default class TokenAuthStrategy extends AuthStrategy {
private token: string;
private tokenManager: TokenManager;

constructor(tokenManager: TokenManager) {
super("token");
this.token = "";
this.tokenManager = tokenManager;
}

async getAuthString(): Promise<string> {
return this.fetchToken()
.then((token) => {
this.token = token;
return `Bearer ${this.token}`;
})
.catch((error) => {
throw new Error(`Failed to fetch access token: ${error}`);
});
}

requiresAuthentication(): boolean {
return true;
}

async fetchToken(): Promise<string> {
if (
this.token == null ||
this.token.length === 0 ||
this.isTokenExpired(this.token)
) {
return this.tokenManager.fetchToken();
}
return this.token;
}

/**
* Function to check if the token is expired with a buffer of 30 seconds.
* @param token - The JWT token as a string.
* @returns Boolean indicating if the token is expired.
*/
isTokenExpired(token: string): boolean {
try {
// Decode the token without verifying the signature, as we only want to read the expiration for this check
const decoded = jwt.decode(token) as JwtPayload;

if (!decoded || !decoded.exp) {
// If the token doesn't have an expiration, consider it expired
return true;
}

const expiresAt = decoded.exp * 1000;
const bufferMilliseconds = 30 * 1000;
const bufferExpiresAt = expiresAt - bufferMilliseconds;

// Return true if the current time is after the expiration time with buffer
return Date.now() > bufferExpiresAt;
} catch (error) {
// If there's an error decoding the token, consider it expired
return true;
}
}
}
50 changes: 41 additions & 9 deletions src/base/BaseTwilio.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import RequestClient from "./RequestClient"; /* jshint ignore:line */
import { HttpMethod } from "../interfaces"; /* jshint ignore:line */
import { Headers } from "../http/request"; /* jshint ignore:line */
import AuthStrategy from "../auth_strategy/AuthStrategy"; /* jshint ignore:line */
import CredentialProvider from "../credential_provider/CredentialProvider"; /* jshint ignore:line */

const os = require("os"); /* jshint ignore:line */
const url = require("url"); /* jshint ignore:line */
Expand Down Expand Up @@ -40,6 +42,7 @@ namespace Twilio {
uri?: string;
username?: string;
password?: string;
authStrategy?: AuthStrategy;
headers?: Headers;
params?: object;
data?: object;
Expand All @@ -56,9 +59,10 @@ namespace Twilio {
/* jshint ignore:end */

export class Client {
username: string;
password: string;
username?: string;
password?: string;
accountSid: string;
credentialProvider?: CredentialProvider;
opts?: ClientOpts;
env?: NodeJS.ProcessEnv;
edge?: string;
Expand Down Expand Up @@ -101,23 +105,29 @@ namespace Twilio {
/* jshint ignore:end */

constructor(username?: string, password?: string, opts?: ClientOpts) {
this.opts = opts || {};
this.env = this.opts.env || {};
this.setOpts(opts);
this.username =
username ??
this.env.TWILIO_ACCOUNT_SID ??
this.env?.TWILIO_ACCOUNT_SID ??
process.env.TWILIO_ACCOUNT_SID ??
(() => {
throw new Error("username is required");
})();
this.password =
password ??
this.env.TWILIO_AUTH_TOKEN ??
this.env?.TWILIO_AUTH_TOKEN ??
process.env.TWILIO_AUTH_TOKEN ??
(() => {
throw new Error("password is required");
})();
this.accountSid = this.opts.accountSid || this.username;
this.accountSid = "";
this.setAccountSid(this.opts?.accountSid || this.username);
this.invalidateOAuth();
}

setOpts(opts?: ClientOpts) {
this.opts = opts || {};
this.env = this.opts.env || {};
this.edge =
this.opts.edge ?? this.env.TWILIO_EDGE ?? process.env.TWILIO_EDGE;
this.region =
Expand All @@ -144,16 +154,35 @@ namespace Twilio {
if (this.opts.lazyLoading === false) {
this._httpClient = this.httpClient;
}
}

setAccountSid(accountSid: string) {
this.accountSid = accountSid;

if (!this.accountSid.startsWith("AC")) {
const apiKeyMsg = this.accountSid.startsWith("SK")
if (this.accountSid && !this.accountSid?.startsWith("AC")) {
const apiKeyMsg = this.accountSid?.startsWith("SK")
? ". The given SID indicates an API Key which requires the accountSid to be passed as an additional option"
: "";

throw new Error("accountSid must start with AC" + apiKeyMsg);
}
}

setCredentialProvider(credentialProvider: CredentialProvider) {
this.credentialProvider = credentialProvider;
this.accountSid = "";
this.invalidateBasicAuth();
}

invalidateBasicAuth() {
this.username = undefined;
this.password = undefined;
}

invalidateOAuth() {
this.credentialProvider = undefined;
}

get httpClient() {
if (!this._httpClient) {
this._httpClient = new RequestClient({
Expand Down Expand Up @@ -196,6 +225,8 @@ namespace Twilio {

const username = opts.username || this.username;
const password = opts.password || this.password;
const authStrategy =
opts.authStrategy || this.credentialProvider?.toAuthStrategy();

const headers = opts.headers || {};

Expand Down Expand Up @@ -235,6 +266,7 @@ namespace Twilio {
uri: uri.href,
username: username,
password: password,
authStrategy: authStrategy,
headers: headers,
params: opts.params,
data: opts.data,
Expand Down
12 changes: 10 additions & 2 deletions src/base/RequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import qs from "qs";
import * as https from "https";
import Response from "../http/response";
import Request, {
RequestOptions as LastRequestOptions,
Headers,
RequestOptions as LastRequestOptions,
} from "../http/request";
import AuthStrategy from "../auth_strategy/AuthStrategy";

const DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded";
const DEFAULT_TIMEOUT = 30000;
Expand Down Expand Up @@ -149,6 +150,7 @@ class RequestClient {
* @param opts.uri - The request uri
* @param opts.username - The username used for auth
* @param opts.password - The password used for auth
* @param opts.authStrategy - The authStrategy for API call
* @param opts.headers - The request headers
* @param opts.params - The request params
* @param opts.data - The request data
Expand All @@ -157,7 +159,7 @@ class RequestClient {
* @param opts.forever - Set to true to use the forever-agent
* @param opts.logLevel - Show debug logs
*/
request<TData>(
async request<TData>(
opts: RequestClient.RequestOptions<TData>
): Promise<Response<TData>> {
if (!opts.method) {
Expand All @@ -180,6 +182,8 @@ class RequestClient {
"base64"
);
headers.Authorization = "Basic " + auth;
} else if (opts.authStrategy) {
headers.Authorization = await opts.authStrategy.getAuthString();
}

const options: AxiosRequestConfig = {
Expand Down Expand Up @@ -296,6 +300,10 @@ namespace RequestClient {
* The password used for auth
*/
password?: string;
/**
* The AuthStrategy for API Call
*/
authStrategy?: AuthStrategy;
/**
* The request headers
*/
Expand Down
66 changes: 66 additions & 0 deletions src/credential_provider/ClientCredentialProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import CredentialProvider from "./CredentialProvider";
import TokenManager from "../http/bearer_token/TokenManager";
import AuthStrategy from "../auth_strategy/AuthStrategy";
import ApiTokenManager from "../http/bearer_token/ApiTokenManager";
import TokenAuthStrategy from "../auth_strategy/TokenAuthStrategy";

class ClientCredentialProvider extends CredentialProvider {
grantType: string;
clientId: string;
clientSecret: string;
tokenManager: TokenManager | null;

constructor() {
super("client-credentials");
this.grantType = "client_credentials";
this.clientId = "";
this.clientSecret = "";
this.tokenManager = null;
}

public toAuthStrategy(): AuthStrategy {
if (this.tokenManager == null) {
this.tokenManager = new ApiTokenManager({
grantType: this.grantType,
clientId: this.clientId,
clientSecret: this.clientSecret,
});
}
return new TokenAuthStrategy(this.tokenManager);
}
}

namespace ClientCredentialProvider {
export class ClientCredentialProviderBuilder {
private readonly instance: ClientCredentialProvider;

constructor() {
this.instance = new ClientCredentialProvider();
}

public setClientId(clientId: string): ClientCredentialProviderBuilder {
this.instance.clientId = clientId;
return this;
}

public setClientSecret(
clientSecret: string
): ClientCredentialProviderBuilder {
this.instance.clientSecret = clientSecret;
return this;
}

public setTokenManager(
tokenManager: TokenManager
): ClientCredentialProviderBuilder {
this.instance.tokenManager = tokenManager;
return this;
}

public build(): ClientCredentialProvider {
return this.instance;
}
}
}

export = ClientCredentialProvider;
9 changes: 9 additions & 0 deletions src/credential_provider/CredentialProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import AuthStrategy from "../auth_strategy/AuthStrategy";

export default abstract class CredentialProvider {
private authType: string;
protected constructor(authType: string) {
this.authType = authType;
}
abstract toAuthStrategy(): AuthStrategy;
}
Loading
Loading