From b56c45db39640a8005df7607ca346d7e3fc03474 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sun, 23 Nov 2025 14:31:52 +0700 Subject: [PATCH 1/3] [client] add JWT auth support --- src/client.test.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++- src/client.ts | 67 ++++++++++++++++++-- 2 files changed, 210 insertions(+), 6 deletions(-) diff --git a/src/client.test.ts b/src/client.test.ts index 733fd7e..21f5fc7 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -12,7 +12,7 @@ import { ProcessState, RegisterWebHookRequest, WebHook, - WebHookEventType + WebHookEventType, } from './domain'; import { HttpClient } from './http'; @@ -341,6 +341,153 @@ describe('Client', () => { expect(result).toBe(undefined); }); + // JWT Authentication Tests + describe('Client with JWT Authentication', () => { + let client: Client; + let mockHttpClient: HttpClient; + const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ'; + + beforeEach(() => { + mockHttpClient = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + } as unknown as HttpClient; + + client = new Client('', jwtToken, mockHttpClient); + }); + + it('creates client with JWT authentication', () => { + expect(client).toBeDefined(); + }); + + it('sends a message with JWT authentication', async () => { + const message: Message = { + message: 'Hello', + phoneNumbers: ['+1234567890'], + }; + const expectedState: MessageState = { + id: '123', + state: ProcessState.Pending, + recipients: [ + { + phoneNumber: '+1234567890', + state: ProcessState.Pending, + } + ] + }; + + (mockHttpClient.post as jest.Mock).mockResolvedValue(expectedState); + + const result = await client.send(message); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `${BASE_URL}/message`, + message, + { + "Content-Type": "application/json", + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: `Bearer ${jwtToken}`, + }, + ); + expect(result).toBe(expectedState); + }); + + it('gets the state of a message with JWT authentication', async () => { + const messageId = '123'; + const expectedState: MessageState = { + id: '123', + state: ProcessState.Pending, + recipients: [ + { + phoneNumber: '+1234567890', + state: ProcessState.Pending, + } + ] + }; + + (mockHttpClient.get as jest.Mock).mockResolvedValue(expectedState); + + const result = await client.getState(messageId); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `${BASE_URL}/message/${messageId}`, + { + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: `Bearer ${jwtToken}`, + }, + ); + expect(result).toBe(expectedState); + }); + + it('throws error when JWT token is missing', () => { + expect(() => { + new Client('', '', mockHttpClient); + }).toThrow('Token is required for JWT authentication'); + }); + }); + + // Backward Compatibility Tests + describe('Client Backward Compatibility', () => { + let client: Client; + let mockHttpClient: HttpClient; + + beforeEach(() => { + mockHttpClient = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + } as unknown as HttpClient; + client = new Client('login', 'password', mockHttpClient); + }); + + it('creates client with Basic Auth using legacy constructor', () => { + expect(client).toBeDefined(); + }); + + it('sends a message with Basic Auth using legacy constructor', async () => { + const message: Message = { + message: 'Hello', + phoneNumbers: ['+1234567890'], + }; + const expectedState: MessageState = { + id: '123', + state: ProcessState.Pending, + recipients: [ + { + phoneNumber: '+1234567890', + state: ProcessState.Pending, + } + ] + }; + + (mockHttpClient.post as jest.Mock).mockResolvedValue(expectedState); + + const result = await client.send(message); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `${BASE_URL}/message`, + message, + { + "Content-Type": "application/json", + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: expect.stringMatching(/^Basic /), + }, + ); + expect(result).toBe(expectedState); + }); + + it('throws error when password is missing in legacy constructor', () => { + expect(() => { + new Client('login', '', mockHttpClient); + }).toThrow('Password is required when using Basic Auth with login'); + }); + }); + it('patches settings', async () => { const settings: Partial = { messages: { limitValue: 200 }, diff --git a/src/client.ts b/src/client.ts index d5f32c9..d10246e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,20 +19,77 @@ export class Client { private defaultHeaders: Record; /** - * @param login The login to use for authentication - * @param password The password to use for authentication + * @param login The login to use for authentication, pass empty string for JWT + * @param password The password or JWT to use for authentication * @param httpClient The HTTP client to use for requests * @param baseUrl The base URL to use for requests. Defaults to {@link BASE_URL}. */ - constructor(login: string, password: string, httpClient: HttpClient, baseUrl = BASE_URL) { + constructor( + login: string, + password: string, + httpClient?: HttpClient, + baseUrl = BASE_URL + ) { this.baseUrl = baseUrl; - this.httpClient = httpClient; + this.httpClient = httpClient || this.getDefaultHttpClient(); this.defaultHeaders = { "User-Agent": "android-sms-gateway/3.0 (client; js)", - "Authorization": `Basic ${btoa(`${login}:${password}`)}`, + }; + + if (login === "") { + if (password === "") { + throw new Error("Token is required for JWT authentication"); + } + this.defaultHeaders["Authorization"] = `Bearer ${password}`; + } else { + if (password === "") { + throw new Error("Password is required when using Basic Auth with login"); + } + this.defaultHeaders["Authorization"] = `Basic ${btoa(`${login}:${password}`)}`; } } + /** + * Gets the default HTTP client implementation + */ + private getDefaultHttpClient(): HttpClient { + // This would typically be implemented elsewhere, but we'll provide a basic implementation + return { + get: async (url: string, headers?: Record): Promise => { + const response = await fetch(url, { method: 'GET', headers }); + return response.json(); + }, + post: async (url: string, body: any, headers?: Record): Promise => { + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(body) + }); + return response.json(); + }, + put: async (url: string, body: any, headers?: Record): Promise => { + const response = await fetch(url, { + method: 'PUT', + headers, + body: JSON.stringify(body) + }); + return response.json(); + }, + patch: async (url: string, body: any, headers?: Record): Promise => { + const response = await fetch(url, { + method: 'PATCH', + headers, + body: JSON.stringify(body) + }); + return response.json(); + }, + delete: async (url: string, headers?: Record): Promise => { + const response = await fetch(url, { method: 'DELETE', headers }); + return response.json(); + }, + }; + } + /** * Sends a new message to the API * @param request - The message to send From 6365bd8cb924431edab582967fb1c525b84f614d Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sun, 23 Nov 2025 14:46:45 +0700 Subject: [PATCH 2/3] [client] add JWT management methods --- src/client.test.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++-- src/client.ts | 61 +++++++++++++++++++++++++---- src/domain.ts | 40 +++++++++++++++++++ 3 files changed, 188 insertions(+), 10 deletions(-) diff --git a/src/client.test.ts b/src/client.test.ts index 21f5fc7..8fbf696 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -11,6 +11,8 @@ import { MessageState, ProcessState, RegisterWebHookRequest, + TokenRequest, + TokenResponse, WebHook, WebHookEventType, } from './domain'; @@ -345,7 +347,7 @@ describe('Client', () => { describe('Client with JWT Authentication', () => { let client: Client; let mockHttpClient: HttpClient; - const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ'; + const jwtToken = 'fake-token-123'; beforeEach(() => { mockHttpClient = { @@ -389,7 +391,7 @@ describe('Client', () => { { "Content-Type": "application/json", "User-Agent": "android-sms-gateway/3.0 (client; js)", - Authorization: `Bearer ${jwtToken}`, + Authorization: `Bearer fake-token-123`, }, ); expect(result).toBe(expectedState); @@ -416,7 +418,7 @@ describe('Client', () => { `${BASE_URL}/message/${messageId}`, { "User-Agent": "android-sms-gateway/3.0 (client; js)", - Authorization: `Bearer ${jwtToken}`, + Authorization: `Bearer fake-token-123`, }, ); expect(result).toBe(expectedState); @@ -508,4 +510,93 @@ describe('Client', () => { ); expect(result).toBe(undefined); }); + + // JWT Token Management Tests + describe('JWT Token Management', () => { + let client: Client; + let mockHttpClient: HttpClient; + + beforeEach(() => { + mockHttpClient = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + } as unknown as HttpClient; + client = new Client('login', 'password', mockHttpClient); + }); + + it('generates a new token', async () => { + const tokenRequest: TokenRequest = { + scopes: ['read', 'write'], + ttl: 3600, + }; + const expectedResponse: TokenResponse = { + access_token: 'fake-token-123', + token_type: 'Bearer', + id: 'token-id-123', + expires_at: '2024-12-31T23:59:59Z', + }; + + (mockHttpClient.post as jest.Mock).mockResolvedValue(expectedResponse); + + const result = await client.generateToken(tokenRequest); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `${BASE_URL}/auth/token`, + tokenRequest, + { + "Content-Type": "application/json", + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: expect.any(String), + }, + ); + expect(result).toBe(expectedResponse); + }); + + it('generates a new token without TTL', async () => { + const tokenRequest: TokenRequest = { + scopes: ['read'], + }; + const expectedResponse: TokenResponse = { + access_token: 'fake-token-123', + token_type: 'Bearer', + id: 'token-id-456', + expires_at: '2024-12-31T23:59:59Z', + }; + + (mockHttpClient.post as jest.Mock).mockResolvedValue(expectedResponse); + + const result = await client.generateToken(tokenRequest); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `${BASE_URL}/auth/token`, + tokenRequest, + { + "Content-Type": "application/json", + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: expect.any(String), + }, + ); + expect(result).toBe(expectedResponse); + }); + + it('revokes a token', async () => { + const jti = 'token-id-123'; + + (mockHttpClient.delete as jest.Mock).mockResolvedValue(undefined); + + const result = await client.revokeToken(jti); + + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `${BASE_URL}/auth/token/${jti}`, + { + "User-Agent": "android-sms-gateway/3.0 (client; js)", + Authorization: expect.any(String), + }, + ); + expect(result).toBe(undefined); + }); + }); }); \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index d10246e..1f4849d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7,7 +7,9 @@ import { DeviceSettings, HealthResponse, LogEntry, - MessagesExportRequest + MessagesExportRequest, + TokenRequest, + TokenResponse } from "./domain"; import { HttpClient } from "./http"; @@ -53,11 +55,28 @@ export class Client { * Gets the default HTTP client implementation */ private getDefaultHttpClient(): HttpClient { - // This would typically be implemented elsewhere, but we'll provide a basic implementation + const handleResponse = async (response: Response): Promise => { + if (response.status === 204) { + return null; + } + + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP error ${response.status}: ${text}`); + } + + const contentType = response.headers.get("Content-Type"); + if (contentType && contentType.includes("application/json")) { + return await response.json(); + } else { + return await response.text(); + } + }; + return { get: async (url: string, headers?: Record): Promise => { const response = await fetch(url, { method: 'GET', headers }); - return response.json(); + return handleResponse(response); }, post: async (url: string, body: any, headers?: Record): Promise => { const response = await fetch(url, { @@ -65,7 +84,7 @@ export class Client { headers, body: JSON.stringify(body) }); - return response.json(); + return handleResponse(response); }, put: async (url: string, body: any, headers?: Record): Promise => { const response = await fetch(url, { @@ -73,7 +92,7 @@ export class Client { headers, body: JSON.stringify(body) }); - return response.json(); + return handleResponse(response); }, patch: async (url: string, body: any, headers?: Record): Promise => { const response = await fetch(url, { @@ -81,11 +100,11 @@ export class Client { headers, body: JSON.stringify(body) }); - return response.json(); + return handleResponse(response); }, delete: async (url: string, headers?: Record): Promise => { const response = await fetch(url, { method: 'DELETE', headers }); - return response.json(); + return handleResponse(response); }, }; } @@ -287,4 +306,32 @@ export class Client { return this.httpClient.patch(url, settings, headers); } + + /** + * Generate a new JWT token with specified scopes and TTL + * @param request - The token request parameters + * @returns The generated token response + */ + async generateToken(request: TokenRequest): Promise { + const url = `${this.baseUrl}/auth/token`; + const headers = { + "Content-Type": "application/json", + ...this.defaultHeaders, + }; + + return this.httpClient.post(url, request, headers); + } + + /** + * Revoke a JWT token by its ID + * @param jti - The JWT token ID to revoke + */ + async revokeToken(jti: string): Promise { + const url = `${this.baseUrl}/auth/token/${jti}`; + const headers = { + ...this.defaultHeaders, + }; + + return this.httpClient.delete(url, headers); + } } \ No newline at end of file diff --git a/src/domain.ts b/src/domain.ts index 8d75d34..70be11f 100644 --- a/src/domain.ts +++ b/src/domain.ts @@ -471,6 +471,46 @@ export interface MessagesExportRequest { until: Date; } +/** + * Represents a request to generate a new JWT token. + */ +export interface TokenRequest { + /** + * The scopes to include in the token. + */ + scopes: string[]; + + /** + * The time-to-live (TTL) of the token in seconds. + */ + ttl?: number; +} + +/** + * Represents a response containing a new JWT token. + */ +export interface TokenResponse { + /** + * The JWT access token. + */ + access_token: string; + + /** + * The type of the token. + */ + token_type: string; + + /** + * The unique identifier of the token. + */ + id: string; + + /** + * The expiration time of the token. + */ + expires_at: string; +} + /** * Represents the payload of a webhook event. */ From 23653ed26d623eef4ab9cca611d951bbf0fcdcd0 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sun, 23 Nov 2025 15:08:44 +0700 Subject: [PATCH 3/3] [docs] add JWT section to README --- README.md | 208 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 153 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 3add11a..35aa133 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# πŸ“± SMS Gateway for Androidβ„’ JS/TS API Client +# πŸ“± SMSGate JS/TS API Client [![npm Version](https://img.shields.io/npm/v/android-sms-gateway.svg?style=for-the-badge)](https://www.npmjs.com/package/android-sms-gateway) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=for-the-badge)](https://github.com/android-sms-gateway/client-ts/blob/master/LICENSE) @@ -7,14 +7,17 @@ [![GitHub Stars](https://img.shields.io/github/stars/android-sms-gateway/client-ts.svg?style=for-the-badge)](https://github.com/android-sms-gateway/client-ts/stargazers) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg?style=for-the-badge)](https://www.typescriptlang.org/) -A TypeScript-first client for seamless integration with the [SMS Gateway for Android](https://sms-gate.app) API. Programmatically send SMS messages through your Android devices with strict typing and modern JavaScript features. +A TypeScript-first client for seamless integration with the [SMSGate](https://sms-gate.app) API. Programmatically send SMS messages through your Android devices with strict typing and modern JavaScript features. **Note**: The API doesn't provide CORS headers, so the library cannot be used in a browser environment directly. ## πŸ“– Table of Contents -- [πŸ“± SMS Gateway for Androidβ„’ JS/TS API Client](#-sms-gateway-for-android-jsts-api-client) +- [πŸ“± SMSGate JS/TS API Client](#-smsgate-jsts-api-client) - [πŸ“– Table of Contents](#-table-of-contents) + - [πŸ” Authentication](#-authentication) + - [Basic Authentication](#basic-authentication) + - [JWT Authentication](#jwt-authentication) - [✨ Features](#-features) - [βš™οΈ Requirements](#️-requirements) - [πŸ“¦ Installation](#-installation) @@ -28,6 +31,7 @@ A TypeScript-first client for seamless integration with the [SMS Gateway for And - [Settings Management](#settings-management) - [πŸ€– Client Guide](#-client-guide) - [Client Configuration](#client-configuration) + - [Authentication Configuration](#authentication-configuration) - [Core Methods](#core-methods) - [Type Definitions](#type-definitions) - [🌐 HTTP Clients](#-http-clients) @@ -37,6 +41,28 @@ A TypeScript-first client for seamless integration with the [SMS Gateway for And - [Development Setup](#development-setup) - [πŸ“„ License](#-license) +## πŸ” Authentication + +The SMSGate client supports two authentication methods: **Basic Authentication** and **JWT (JSON Web Token) Authentication**. JWT is the recommended approach for production environments due to its enhanced security features and support for scoped permissions. + +### Basic Authentication + +Basic Authentication uses a username and password to access the API. This method is simple but less secure for production use. + +**When to use:** +- Simple integrations +- Development and testing +- Legacy systems + +### JWT Authentication + +JWT Authentication uses bearer tokens with configurable scopes to access the API. This method provides enhanced security and fine-grained access control. + +**When to use:** +- Production environments +- Applications requiring scoped permissions +- Systems with multiple components needing different access levels + ## ✨ Features - **TypeScript Ready**: Full type definitions out of the box @@ -73,75 +99,80 @@ bun add android-sms-gateway ```typescript import Client from 'android-sms-gateway'; -// Create a fetch-based HTTP client -const httpFetchClient = { - get: async (url, headers) => { - const response = await fetch(url, { - method: "GET", - headers - }); - - return response.json(); - }, - post: async (url, body, headers) => { - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(body) - }); - - return response.json(); - }, - delete: async (url, headers) => { - const response = await fetch(url, { - method: "DELETE", - headers - }); - - return response.json(); - } -}; - -// Initialize client -const api = new Client( +// First, create a client with Basic Auth to generate a JWT token +const basicAuthClient = new Client( process.env.ANDROID_SMS_GATEWAY_LOGIN!, - process.env.ANDROID_SMS_GATEWAY_PASSWORD!, - httpFetchClient + process.env.ANDROID_SMS_GATEWAY_PASSWORD! ); -// Send message -const message = { - phoneNumbers: ['+1234567890'], - message: 'Secure OTP: 123456 πŸ”' -}; +// Generate a JWT token with specific scopes +async function generateJWTToken() { + try { + const tokenRequest = { + scopes: [ + "messages:send", + "messages:read", + "devices:list" + ], + ttl: 3600 // Token expires in 1 hour + }; + + const tokenResponse = await basicAuthClient.generateToken(tokenRequest); + console.log('JWT Token generated, expires at:', tokenResponse.expires_at); + return tokenResponse.access_token; + } catch (error) { + console.error('Token generation failed:', error); + throw error; + } +} +// Initialize client with JWT Authentication +async function initializeJWTClient() { + const jwtToken = await generateJWTToken(); + + // Initialize client with JWT token (empty string for login, token for password) + const jwtClient = new Client( + "", // Empty string for login when using JWT + jwtToken // JWT token + ); + + return jwtClient; +} + +// Send message using JWT Authentication async function sendSMS() { try { - const state = await api.send(message); + const jwtClient = await initializeJWTClient(); + + const message = { + phoneNumbers: ['+1234567890'], + message: 'Secure OTP: 123456 πŸ”' + }; + + const state = await jwtClient.send(message); console.log('Message ID:', state.id); - + // Check status after 5 seconds setTimeout(async () => { - const updatedState = await api.getState(state.id); - console.log('Message status:', updatedState.status); + const updatedState = await jwtClient.getState(state.id); + console.log('Message status:', updatedState.state); }, 5000); } catch (error) { console.error('Sending failed:', error); } } -// Send message with skipPhoneValidation -async function sendSMSWithSkipValidation() { +// Revoke a JWT token +async function revokeJWTToken(jti: string) { try { - const state = await api.send(message, { skipPhoneValidation: true }); - console.log('Message ID (with skip validation):', state.id); + await basicAuthClient.revokeToken(jti); + console.log('JWT token revoked successfully'); } catch (error) { - console.error('Sending failed:', error); + console.error('Token revocation failed:', error); } } sendSMS(); -sendSMSWithSkipValidation(); ``` ### Webhook Management @@ -248,11 +279,33 @@ The `Client` class accepts the following constructor arguments: | Argument | Description | Default | | ------------ | -------------------------- | ---------------------------------------- | -| `login` | Username | **Required** | -| `password` | Password | **Required** | -| `httpClient` | HTTP client implementation | **Required** | +| `login` | Username or empty string | **Required** | +| `password` | Password or JWT token | **Required** | +| `httpClient` | HTTP client implementation | `fetch` | | `baseUrl` | API base URL | `"https://api.sms-gate.app/3rdparty/v1"` | +#### Authentication Configuration + +**Basic Authentication:** +```typescript +const api = new Client( + process.env.ANDROID_SMS_GATEWAY_LOGIN!, // Username + process.env.ANDROID_SMS_GATEWAY_PASSWORD! // Password +); +``` + +**JWT Authentication:** +```typescript +const api = new Client( + "", // Empty string for login when using JWT + jwtToken // JWT token +); +``` + +The client automatically detects which authentication method to use based on the `login` parameter: +- If `login` is a non-empty string: Uses Basic Authentication +- If `login` is an empty string: Uses JWT Authentication with the provided token + ### Core Methods | Method | Description | Returns | @@ -283,6 +336,10 @@ The `Client` class accepts the following constructor arguments: | `getSettings()` | Get settings | `Promise` | | `updateSettings(settings: DeviceSettings)` | Update settings | `Promise` | | `patchSettings(settings: Partial)` | Partially update settings | `Promise` | +| | | | +| **JWT Token Management** | | | +| `generateToken(request: TokenRequest)` | Generate new JWT token | `Promise` | +| `revokeToken(jti: string)` | Revoke JWT token by ID | `Promise` | ### Type Definitions @@ -348,13 +405,49 @@ interface MessagesExportRequest { since: string; until: string; } + +// JWT Authentication Types + +interface TokenRequest { + /** + * The scopes to include in the token. + */ + scopes: string[]; + + /** + * The time-to-live (TTL) of the token in seconds. + */ + ttl?: number; +} + +interface TokenResponse { + /** + * The JWT access token. + */ + access_token: string; + + /** + * The type of the token. + */ + token_type: string; + + /** + * The unique identifier of the token. + */ + id: string; + + /** + * The expiration time of the token. + */ + expires_at: string; +} ``` For more details, see the [`domain.ts`](./src/domain.ts). ## 🌐 HTTP Clients -The library doesn't come with built-in HTTP clients. Instead, you should provide your own implementation of the `HttpClient` interface: +The library comes with fetch-based built-in HTTP client. You can provide your own implementation of the `HttpClient` interface: ```typescript interface HttpClient { @@ -373,6 +466,11 @@ interface HttpClient { - Always store credentials in environment variables - Never expose credentials in client-side code - Use HTTPS for all production communications +- Rotate passwords regularly +- Use strong, unique passwords +- Use appropriate TTL values based on your security requirements +- Apply the principle of least privilege +- Implement proper token revocation workflows ## πŸ“š API Reference