Skip to content

Commit

Permalink
chore: add support for oauth in public apis (#1050)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiwarishubham635 authored Dec 9, 2024
1 parent b463f4e commit 1299c58
Show file tree
Hide file tree
Showing 26 changed files with 3,019 additions and 20 deletions.
11 changes: 11 additions & 0 deletions src/auth_strategy/AuthStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default abstract class AuthStrategy {
private authType: string;
protected constructor(authType: string) {
this.authType = authType;
}
getAuthType(): string {
return this.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.message}`);
});
}

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 Promise.resolve(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;
}
}
}
76 changes: 58 additions & 18 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,23 @@ 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 ??
process.env.TWILIO_ACCOUNT_SID ??
(() => {
throw new Error("username is required");
})();
this.env?.TWILIO_ACCOUNT_SID ??
process.env.TWILIO_ACCOUNT_SID;
this.password =
password ??
this.env.TWILIO_AUTH_TOKEN ??
process.env.TWILIO_AUTH_TOKEN ??
(() => {
throw new Error("password is required");
})();
this.accountSid = this.opts.accountSid || this.username;
this.env?.TWILIO_AUTH_TOKEN ??
process.env.TWILIO_AUTH_TOKEN;
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 +148,35 @@ namespace Twilio {
if (this.opts.lazyLoading === false) {
this._httpClient = this.httpClient;
}
}

if (!this.accountSid.startsWith("AC")) {
const apiKeyMsg = this.accountSid.startsWith("SK")
setAccountSid(accountSid?: string) {
this.accountSid = accountSid || "";

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 +219,22 @@ namespace Twilio {

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

if (!authStrategy) {
if (!username) {
(() => {
throw new Error("username is required");
})();
}

if (!password) {
(() => {
throw new Error("password is required");
})();
}
}

const headers = opts.headers || {};

Expand Down Expand Up @@ -223,7 +262,7 @@ namespace Twilio {
headers["Content-Type"] = "application/x-www-form-urlencoded";
}

if (!headers["Accept"]) {
if (opts.method !== "delete" && !headers["Accept"]) {
headers["Accept"] = "application/json";
}

Expand All @@ -235,6 +274,7 @@ namespace Twilio {
uri: uri.href,
username: username,
password: password,
authStrategy: authStrategy,
headers: headers,
params: opts.params,
data: opts.data,
Expand Down
2 changes: 2 additions & 0 deletions src/base/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ export default class Page<
if (keys.length === 1) {
return payload[keys[0]];
}
for (const key of keys)
if (Array.isArray(payload[key])) return payload[key];

throw new Error("Page Records cannot be deserialized");
}
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;
Loading

0 comments on commit 1299c58

Please sign in to comment.