From c841289a573b5defe04ff6828e1614f650978898 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sun, 23 Nov 2025 17:40:26 +0700 Subject: [PATCH 1/2] [integration] add JWT sections --- docs/faq/authentication.md | 377 +++++++++++++++++++ docs/faq/local-server.md | 6 +- docs/getting-started/local-server.md | 3 + docs/integration/api.md | 27 +- docs/integration/authentication.md | 539 +++++++++++++++++++++++++++ docs/integration/client-libraries.md | 7 + mkdocs.yml | 2 + 7 files changed, 959 insertions(+), 2 deletions(-) create mode 100644 docs/faq/authentication.md create mode 100644 docs/integration/authentication.md diff --git a/docs/faq/authentication.md b/docs/faq/authentication.md new file mode 100644 index 0000000..745330b --- /dev/null +++ b/docs/faq/authentication.md @@ -0,0 +1,377 @@ +# ❌ FAQ - Authentication + +## πŸ” What is JWT authentication and how does it work? + +JWT (JSON Web Token) authentication is the primary authentication mechanism for the SMSGate API. It provides a secure, scalable way to authenticate API requests without transmitting credentials with each request. + +## πŸ”„ How do I migrate from Basic Auth to JWT? + +Migrating from Basic Authentication to JWT provides enhanced security, better performance, and fine-grained access control. Here's how to migrate: + +### Step 1: Update Your Code + +Replace Basic Auth with JWT Bearer tokens: + +=== "Before (Basic Auth)" + ```python + response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/messages", + auth=("username", "password"), + json={"phoneNumbers": ["+1234567890"], "textMessage": {"text": "Hello world!"}} + ) + ``` + +=== "After (JWT)" + ```python + # First, get a token + token_response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/auth/token", + auth=("username", "password"), + json={"ttl": 3600, "scopes": ["messages:send"]} + ) + + if token_response.status_code == 201: + token_data = token_response.json() + access_token = token_data["access_token"] + + # Then use the token + response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/messages", + headers={ + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + }, + json={"phoneNumbers": ["+1234567890"], "textMessage": {"text": "Hello world!"}} + ) + ``` + +### Step 2: Implement Token Management + +- **Token Refresh**: Implement automatic token refresh before expiration +- **Error Handling**: Handle 401/403 errors gracefully +- **Secure Storage**: Store tokens securely on the server side + +## πŸ”‘ What are JWT scopes and how do I use them? + +JWT scopes define the permissions associated with a token, implementing the principle of least privilege. All scopes follow the pattern: `resource:action` + +All available scopes are listed in the [Authentication](../integration/authentication.md#jwt-scopes-) section. + +### Using Scopes + +When requesting a JWT token, specify which scopes you need: + +```json +{ + "ttl": 3600, + "scopes": [ + "messages:send", + "messages:read", + "devices:list" + ] +} +``` + +!!! tip "Scope Best Practices" + - Request only the scopes you need + - Create multiple tokens with different scopes for different components + - Use short TTLs for tokens with sensitive scopes + - Avoid using `all:any` unless absolutely necessary + +## ⏰ How long do JWT tokens last and how do I refresh them? + +JWT tokens have a configurable time-to-live (TTL). The default TTL is 24 hours (86400 seconds), but you can specify a custom duration when generating a token. + +### Token Expiration + +```json +{ + "id": "w8pxz0a4Fwa4xgzyCvSeC", + "token_type": "Bearer", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": "2025-11-22T08:45:00Z" +} +``` + +### Token Refresh Strategy + +Since JWT tokens cannot be refreshed (they must be reissued), implement a proactive refresh strategy: + +```python +import requests +from datetime import datetime, timedelta + +class SMSGatewayClient: + def __init__(self, gateway_url, username, password): + self.gateway_url = gateway_url + self.username = username + self.password = password + self.access_token = None + self.token_expires_at = None + + def get_token(self, scopes, ttl=3600): + """Get a new JWT token""" + response = requests.post( + f"{self.gateway_url}/3rdparty/v1/auth/token", + auth=(self.username, self.password), + headers={"Content-Type": "application/json"}, + json={"ttl": ttl, "scopes": scopes} + ) + + if response.status_code == 201: + token_data = response.json() + self.access_token = token_data["access_token"] + self.token_expires_at = datetime.fromisoformat( + token_data["expires_at"].replace("Z", "+00:00") + ) + return self.access_token + else: + raise Exception(f"Failed to get token: {response.status_code}") + + def ensure_valid_token(self, scopes): + """Ensure we have a valid token, refresh if needed""" + if (self.access_token is None or + self.token_expires_at is None or + datetime.now() + timedelta(minutes=5) >= self.token_expires_at): + return self.get_token(scopes) + return self.access_token +``` + +!!! tip "Token Management Best Practices" + - Refresh tokens 5-10 minutes before expiration + - Implement exponential backoff for failed refresh attempts + - Store tokens securely (not in client-side code) + +## πŸ›‘οΈ How do I revoke a JWT token? + +JWT tokens can be revoked before they expire using the token revocation endpoint. This is useful when a token is compromised or no longer needed. + +### Revoking a Token + +```bash +curl -X DELETE "https://api.sms-gate.app/3rdparty/v1/auth/token/{jti}" \ + -H "Authorization: Basic username:password" +``` + +Where `{jti}` is the token ID from the token response. + +## πŸ” "Invalid token" JWT Error + +The "invalid token" error occurs when the JWT token is malformed, has an incorrect signature, or cannot be validated by the server. + +### Common Causes + +1. **Malformed Token**: The token structure is incorrect or corrupted +2. **Invalid Signature**: The token signature doesn't match the server's secret +3. **Algorithm Mismatch**: The token was signed with a different algorithm than expected +4. **Encoding Issues**: The token contains invalid characters or formatting + +### Troubleshooting Steps + +1. **Check Token Format**: Ensure the token has three parts separated by dots (`.`) + ``` + header.payload.signature + ``` + +2. **Verify Token Copy**: Make sure you copied the entire token without extra spaces or line breaks + +3. **Validate Token Structure**: Use an online JWT decoder to verify the token structure + ```bash + # Check token structure + echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | tr '.' '\n' + ``` + +## ⏰ "Token expired" JWT Error + +The "token expired" error occurs when the JWT token has passed its expiration time. This is a normal part of the JWT lifecycle and requires token refresh. + +### Common Causes + +1. **Token TTL Expired**: The token has reached its expiration time +2. **Clock Skew**: Time differences between client and server clocks +3. **Long-running Operations**: Operations that take longer than the token TTL + +### Troubleshooting Steps + +1. **Check Expiration Time**: Parse the token to see when it expires + ```python + import jwt + import datetime + + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + try: + decoded = jwt.decode(token, options={"verify_signature": False}) + exp_time = datetime.datetime.fromtimestamp(decoded['exp']) + print(f"Token expires at: {exp_time}") + except Exception as e: + print(f"Error decoding token: {e}") + ``` + +2. **Implement Token Refresh**: Refresh tokens before they expire + ```python + # Refresh token 5 minutes before expiration + if datetime.now() + timedelta(minutes=5) >= token_expires_at: + new_token = get_new_token() + ``` + +3. **Adjust Token TTL**: Use a longer TTL for long-running operations + ```json + { + "ttl": 7200, // 2 hours instead of 1 hour + "scopes": ["messages:send", "messages:read"] + } + ``` + +!!! tip "Best Practices" + - Implement automatic token refresh + - Use appropriate TTL values for your use case + - Handle token expiration gracefully in your code + - Consider clock skew in your expiration logic + +## 🚫 "Token revoked" JWT Error + +The "token revoked" error occurs when a JWT token has been manually revoked before its natural expiration time. + +### Common Causes + +1. **Manual Revocation**: Token was explicitly revoked by an administrator +2. **Security Incident**: Token was revoked due to a security concern + +### Troubleshooting Steps + +1. **Request New Token**: Generate a new token with the same scopes + ```bash + curl -X POST "https://api.sms-gate.app/3rdparty/v1/auth/token" \ + -u "username:password" \ + -H "Content-Type: application/json" \ + -d '{ + "ttl": 3600, + "scopes": ["messages:send", "messages:read"] + }' + ``` + +2. **Investigate Revocation Reason**: Contact support to understand why the token was revoked + +## πŸ™… "Scope required" JWT Error + +The "scope required" error occurs when the JWT token doesn't have the necessary scope to access a specific resource or perform a specific action. + +### Common Causes + +1. **Missing Scope**: The token doesn't include the required scope +2. **Incorrect Scope**: The token has the wrong scope for the requested action +3. **Scope Typos**: The scope name is misspelled or incorrectly formatted +4. **Resource Changes**: The required scope for a resource has changed + +### Troubleshooting Steps + +1. **Check Token Scopes**: Verify what scopes your token contains + ```python + import jwt + + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + try: + decoded = jwt.decode(token, options={"verify_signature": False}) + print("Token scopes:", decoded.get('scopes', [])) + except Exception as e: + print(f"Error decoding token: {e}") + ``` + +2. **Verify Required Scope**: Check the API documentation for the required scope + ``` + GET /3rdparty/v1/messages requires: messages:list + POST /3rdparty/v1/messages requires: messages:send + ``` + +3. **Request New Token**: Generate a new token with the correct scopes + ```json + { + "ttl": 3600, + "scopes": [ + "messages:send", + "messages:read", + "devices:list" + ] + } + ``` + +!!! tip "Scope Best Practices" + - Request only the scopes you need + - Use the exact scope names from the documentation + - Create multiple tokens for different purposes + - Regularly review and update your scope requirements + +## πŸ”„ "Migration from Basic Auth to JWT" Issues + +When migrating from Basic Authentication to JWT, you may encounter various issues. Here are common problems and their solutions. + +### Common Issues + +1. **Token Generation Errors**: Unable to generate JWT tokens +2. **Permission Errors**: JWT tokens don't have the same permissions as Basic Auth +3. **Code Compatibility**: Existing code doesn't work with JWT authentication + +### Troubleshooting Steps + +1. **Verify Token Generation**: Ensure you can generate JWT tokens successfully + ```bash + curl -X POST "https://api.sms-gate.app/3rdparty/v1/auth/token" \ + -u "username:password" \ + -H "Content-Type: application/json" \ + -d '{"ttl": 3600, "scopes": ["messages:send"]}' + ``` + +2. **Update Code Gradually**: Migrate code incrementally rather than all at once + ```python + # Hybrid approach during migration + def make_request(endpoint, data=None, use_jwt=True): + if use_jwt and jwt_token: + headers = {"Authorization": f"Bearer {jwt_token}"} + else: + # Fall back to Basic Auth + headers = {} + auth = (username, password) + + return requests.post(endpoint, headers=headers, auth=auth, json=data) + ``` + +3. **Test in Staging**: Test JWT authentication in a staging environment before production + +!!! tip "Migration Best Practices" + - Keep Basic Auth as a fallback during transition + - Monitor authentication errors during migration + +## πŸ›‘οΈ JWT Security Issues + +JWT tokens are generally secure, but improper implementation can lead to security vulnerabilities. + +### Common Security Issues + +1. **Long TTLs**: Using excessively long token expiration times +2. **Token Leakage**: Tokens being exposed in logs, browser storage, or network traffic +3. **Insufficient Scopes**: Using overly broad scopes like `all:any` + +### Troubleshooting Steps + +1. **Review Token TTL**: Ensure your token TTL is appropriate for your use case + ```json + { + "ttl": 3600, // 1 hour - reasonable for most use cases + "scopes": ["messages:send"] + } + ``` + +2. **Implement Secure Storage**: Ensure tokens are stored securely + ```python + # Example of secure token storage + from cryptography.fernet import Fernet + + key = Fernet.generate_key() + cipher_suite = Fernet(key) + encrypted_token = cipher_suite.encrypt(jwt_token.encode()) + ``` + +!!! tip "Security Best Practices" + - Use the shortest practical TTL for your use case + - Store tokens securely on the server side + - Implement proper token revocation diff --git a/docs/faq/local-server.md b/docs/faq/local-server.md index 1fb27b1..8247717 100644 --- a/docs/faq/local-server.md +++ b/docs/faq/local-server.md @@ -32,4 +32,8 @@ Attempting to connect to the device's API directly can give you an immediate sen ## πŸ”‘ How do I change my password in Local mode? :material-key: -For Local mode, password management is handled through the [Server Configuration](../getting-started/local-server.md#server-configuration) section. \ No newline at end of file +For Local mode, password management is handled through the [Server Configuration](../getting-started/local-server.md#server-configuration) section. + +## πŸ” What authentication methods are supported in Local Server mode? + +Local Server mode **only supports Basic Authentication**. JWT authentication is not available in this mode. For JWT authentication, please use Public Cloud Server or Private Server modes. diff --git a/docs/getting-started/local-server.md b/docs/getting-started/local-server.md index 50f750d..53cc6fe 100644 --- a/docs/getting-started/local-server.md +++ b/docs/getting-started/local-server.md @@ -13,6 +13,9 @@ This mode is ideal for sending messages from a local network, enabling direct co 3. Tap the status button (labeled `Offline`) at the bottom of the screen to start the server; it will switch to `Online` when running. 4. The `Local Server` section will display your device's local and public IP addresses, as well as the credentials for basic authentication. + !!! warning "Authentication Method" + Local Server mode **only supports Basic Authentication**. JWT authentication is not available in this mode. + !!! note "Public IP Accessibility" The displayed public IP address is only accessible from the internet if your device has a public IP assigned by your ISP and your firewall/router allows connections to the specified port (with port forwarding configured). Many ISPs use Carrier-Grade NAT (CG‑NAT), which prevents direct internet access to devices behind shared addresses. See also: [FAQ β€” Local Server](../faq/local-server.md). diff --git a/docs/integration/api.md b/docs/integration/api.md index bf01857..f7f32f4 100644 --- a/docs/integration/api.md +++ b/docs/integration/api.md @@ -1,6 +1,6 @@ # Integration - API πŸ“± -The SMS Gateway for Androidβ„’ provides a robust API that allows you to send SMS messages programmatically from your own applications or services. This enables seamless integration with your existing infrastructure. +The SMSGate provides a robust API that allows you to send SMS messages programmatically from your own applications or services. This enables seamless integration with your existing infrastructure. ## API Specification πŸ“„ @@ -13,3 +13,28 @@ You can find the OpenAPI specification for our API at the following link: [OpenA - **Cloud API**: Accessible from anywhere on the internet External services like Google Apps Script, AWS Lambda, or other cloud functions **cannot** directly access Local Server API endpoints due to network constraints. + +## Authentication πŸ”’ + +The SMSGate API supports two authentication methods: + +1. **Basic Authentication** (Legacy): Simple username/password authentication for backward compatibility +2. **JWT Bearer Tokens** (Recommended): Modern, secure authentication with fine-grained access control + +!!! tip "Recommendation" + For new integrations, we strongly recommend using JWT authentication as it provides better security, scalability, and fine-grained access control through scopes. See [Authentication Guide](authentication.md) for detailed information. + +### Authentication Comparison + +| Feature | JWT Authentication | Basic Authentication | +| ---------------- | ---------------------------------- | ------------------------------------------- | +| Security | High (token-based with expiration) | Medium (credentials sent with each request) | +| Access Control | Fine-grained (scopes) | Coarse-grained (all or nothing) | +| Token Management | Built-in (revocation, TTL) | None | +| Recommended For | All new integrations | Legacy systems only | + +## See Also πŸ”— + +- [Authentication Guide](authentication.md) - Detailed information about JWT authentication +- [Integration Guide](index.md) - Overview of integration options +- [Client Libraries](client-libraries.md) - Pre-built libraries for various languages diff --git a/docs/integration/authentication.md b/docs/integration/authentication.md new file mode 100644 index 0000000..81a61aa --- /dev/null +++ b/docs/integration/authentication.md @@ -0,0 +1,539 @@ +# Integration - Authentication πŸ”’ + +This guide provides a comprehensive overview of authentication in the SMSGate API. JWT authentication is the primary mechanism for securing API access, providing a robust and scalable way to authenticate requests. + +## Authentication Overview πŸ”‘ + +The SMSGate supports multiple authentication methods to accommodate different use cases: + +- **Basic Authentication**: Legacy username/password for backward compatibility +- **JWT Bearer Tokens**: Primary authentication mechanism with configurable TTL + +JWT authentication is recommended for all new integrations as it provides better security, scalability, and fine-grained access control through scopes. + +## JWT Authentication πŸ” + +JWT authentication uses bearer tokens to authenticate API requests. These tokens contain encoded information about the user, their permissions (scopes), and token metadata. + +## Token Generation πŸš€ + +To generate a JWT token, make a POST request to the token endpoint using Basic Authentication. + +### Endpoint + +``` +POST /3rdparty/v1/auth/token +``` + +### Request + +```bash +curl -X POST "https://api.sms-gate.app/3rdparty/v1/auth/token" \ + -u "username:password" \ + -H "Content-Type: application/json" \ + -d '{ + "ttl": 3600, + "scopes": [ + "messages:send", + "messages:read", + "devices:list" + ] + }' +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +| --------- | ------- | -------- | --------------------------------------------------------- | +| `ttl` | integer | No | Token time-to-live in seconds (default: server dependent) | +| `scopes` | array | Yes | List of scopes for the token | + +### Response + +```json +{ + "id": "w8pxz0a4Fwa4xgzyCvSeC", + "token_type": "Bearer", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": "2025-11-22T08:45:00Z" +} +``` + +### Response Fields + +| Field | Type | Description | +| -------------- | ------ | ----------------------------------------- | +| `id` | string | Token identifier (JTI) | +| `token_type` | string | Token type (always "Bearer") | +| `access_token` | string | The JWT token | +| `expires_at` | string | ISO 8601 timestamp when the token expires | + +## Using JWT Tokens πŸ“ + +Once you have a JWT token, include it in the Authorization header of your API requests. + +### Authorization Header Format + +``` +Authorization: Bearer +``` + +### Example Request + +```bash +curl -X GET "https://api.sms-gate.app/3rdparty/v1/messages" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +## Token Management πŸ”„ + +### Revoking Tokens + +To revoke a token before it expires, make a DELETE request to the token endpoint. + +``` +DELETE /3rdparty/v1/auth/token/{jti} +``` + +```bash +curl -X DELETE "https://api.sms-gate.app/3rdparty/v1/auth/token/w8pxz0a4Fwa4xgzyCvSeC" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +### Token Best Practices + +- **Use short TTLs**: Set appropriate expiration times based on your security requirements +- **Request minimal scopes**: Only request the scopes your application needs +- **Store tokens securely**: Keep tokens in secure storage, not in client-side code +- **Implement token rotation**: Refresh tokens before they expire for long-running applications +- **Revoke unused tokens**: Immediately revoke tokens that are no longer needed + +## JWT Scopes πŸ” + +Scopes define the permissions associated with a JWT token, implementing the principle of least privilege. + +### Scope Structure + +All scopes follow the pattern: `resource:action` + +- **Resource**: The entity being accessed (e.g., `messages`, `devices`) +- **Action**: The operation being performed (e.g., `send`, `read`, `write`) + +### Available Scopes + +#### Global Scope + +| Scope | Description | Access Level | +| --------- | ---------------------------------------------------- | ------------ | +| `all:any` | Provides full access to all resources and operations | Full Access | + +#### Messages Scopes + +| Scope | Description | Access Level | +| --------------- | --------------------------------------------- | ------------ | +| `messages:send` | Permission to send SMS messages | Write | +| `messages:read` | Permission to read individual message details | Read | +| `messages:list` | Permission to list and view messages | Read | + +#### Devices Scopes + +| Scope | Description | Access Level | +| ---------------- | ---------------------------------------------- | ------------ | +| `devices:list` | Permission to list and view registered devices | Read | +| `devices:delete` | Permission to remove/unregister devices | Delete | + +#### Webhooks Scopes + +| Scope | Description | Access Level | +| ----------------- | ------------------------------------------------------ | ------------ | +| `webhooks:list` | Permission to list and view webhook configurations | Read | +| `webhooks:write` | Permission to create and modify webhook configurations | Write | +| `webhooks:delete` | Permission to remove webhook configurations | Delete | + +#### Settings Scopes + +| Scope | Description | Access Level | +| ---------------- | --------------------------------------------- | ------------ | +| `settings:read` | Permission to read system and user settings | Read | +| `settings:write` | Permission to modify system and user settings | Write | + +#### Logs Scopes + +| Scope | Description | Access Level | +| ----------- | ---------------------------------------------- | ------------ | +| `logs:read` | Permission to read system and application logs | Read | + +#### Token Management Scopes + +| Scope | Description | Access Level | +| --------------- | -------------------------------------------- | -------------- | +| `tokens:manage` | Permission to generate and revoke JWT tokens | Administrative | + +### Scope Assignment by Endpoint + +#### Messages API + +| Endpoint | Method | Required Scope | Description | +| ------------------------------------ | ------ | ----------------- | ------------------- | +| `/3rdparty/v1/messages` | GET | `messages:list` | List messages | +| `/3rdparty/v1/messages` | POST | `messages:send` | Send a new message | +| `/3rdparty/v1/messages/:id` | GET | `messages:read` | Get message details | +| `/3rdparty/v1/messages/inbox/export` | POST | `messages:export` | Export messages | + +#### Devices API + +| Endpoint | Method | Required Scope | Description | +| -------------------------- | ------ | ---------------- | ------------- | +| `/3rdparty/v1/devices` | GET | `devices:list` | List devices | +| `/3rdparty/v1/devices/:id` | DELETE | `devices:delete` | Remove device | + +#### Webhooks API + +| Endpoint | Method | Required Scope | Description | +| --------------------------- | ------ | ----------------- | -------------- | +| `/3rdparty/v1/webhooks` | GET | `webhooks:list` | List webhooks | +| `/3rdparty/v1/webhooks` | POST | `webhooks:write` | Create webhook | +| `/3rdparty/v1/webhooks/:id` | DELETE | `webhooks:delete` | Remove webhook | + +#### Settings API + +| Endpoint | Method | Required Scope | Description | +| ----------------------- | ------ | ---------------- | ---------------- | +| `/3rdparty/v1/settings` | GET | `settings:read` | Get settings | +| `/3rdparty/v1/settings` | PATCH | `settings:write` | Update settings | +| `/3rdparty/v1/settings` | PUT | `settings:write` | Replace settings | + +#### Logs API + +| Endpoint | Method | Required Scope | Description | +| ------------------- | ------ | -------------- | ----------- | +| `/3rdparty/v1/logs` | GET | `logs:read` | Read logs | + +#### Token Management API + +| Endpoint | Method | Required Scope | Description | +| ------------------------------ | ------ | --------------- | ------------------ | +| `/3rdparty/v1/auth/token` | POST | `tokens:manage` | Generate new token | +| `/3rdparty/v1/auth/token/:jti` | DELETE | `tokens:manage` | Revoke token | + +## Code Examples πŸ’» + +=== "cURL" + + ```bash title="Generate JWT Token" + curl -X POST "https://api.sms-gate.app/3rdparty/v1/auth/token" \ + -u "username:password" \ + -H "Content-Type: application/json" \ + -d '{ + "ttl": 3600, + "scopes": ["messages:send", "messages:read"] + }' + ``` + + ```bash title="Use JWT Token" + curl -X POST "https://api.sms-gate.app/3rdparty/v1/messages" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "phoneNumbers": ["+1234567890"], + "textMessage": { + "text": "Hello from JWT!" + } + }' + ``` + +=== "Python" + + ```python title="Generate JWT Token" + import requests + import json + + # Configuration + gateway_url = "https://api.sms-gate.app" + username = "your_username" + password = "your_password" + + # Generate token + response = requests.post( + f"{gateway_url}/3rdparty/v1/auth/token", + auth=(username, password), + headers={"Content-Type": "application/json"}, + data=json.dumps({ + "ttl": 3600, + "scopes": ["messages:send", "messages:read"] + }) + ) + + if response.status_code == 201: + token_data = response.json() + access_token = token_data["access_token"] + print(f"Token generated successfully. Expires at: {token_data['expires_at']}") + else: + print(f"Error generating token: {response.status_code} - {response.text}") + ``` + + ```python title="Send SMS with JWT" + import requests + import json + + # Configuration + gateway_url = "https://api.sms-gate.app" + access_token = "your_jwt_token" + + # Send message + response = requests.post( + f"{gateway_url}/3rdparty/v1/messages", + headers={ + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + }, + data=json.dumps({ + "phoneNumbers": ["+1234567890"], + "textMessage": {"text": "Hello from JWT!"} + }) + ) + + if response.status_code == 202: + print("Message sent successfully!") + else: + print(f"Error sending message: {response.status_code} - {response.text}") + ``` + +=== "JavaScript" + + ```javascript title="Generate JWT Token" + // Configuration + const gatewayUrl = "https://api.sms-gate.app"; + const username = "your_username"; + const password = "your_password"; + + // Generate token + fetch(`${gatewayUrl}/3rdparty/v1/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + btoa(`${username}:${password}`) + }, + body: JSON.stringify({ + ttl: 3600, + scopes: ["messages:send", "messages:read"] + }) + }) + .then(response => response.json()) + .then(data => { + if (data.access_token) { + console.log('Token generated successfully. Expires at:', data.expires_at); + } else { + console.error('Error generating token:', data); + } + }) + .catch(error => console.error('Error:', error)); + ``` + + ```javascript title="Send SMS with JWT" + // Configuration + const gatewayUrl = "https://api.sms-gate.app"; + const accessToken = "your_jwt_token"; + + // Send message + fetch(`${gatewayUrl}/3rdparty/v1/messages`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + phoneNumbers: ["+1234567890"], + textMessage: {text: "Hello from JWT!"} + }) + }) + .then(response => response.json()) + .then(data => { + if (data.id) { + console.log('Message sent successfully!'); + } else { + console.error('Error sending message:', data); + } + }) + .catch(error => console.error('Error:', error)); + ``` + +## Migration from Basic Auth to JWT πŸ”„ + +### Why Migrate? + +- **Enhanced Security**: JWT tokens provide better security than Basic Auth +- **Fine-grained Access Control**: Scopes allow precise permission management + +### Migration Steps + +1. **Generate JWT Tokens**: Use the token endpoint to create JWT tokens with appropriate scopes +2. **Update Client Code**: Replace Basic Auth with JWT Bearer tokens +3. **Implement Token Management**: Add token refresh and error handling +4. **Test Thoroughly**: Ensure all functionality works with JWT authentication + +### Migration Example + +=== "Before (Basic Auth)" + + ```python + # Basic Auth example + response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/messages", + auth=("username", "password"), + json={ + "phoneNumbers": ["+1234567890"], + "textMessage": {"text": "Hello world!"} + } + ) + ``` + +=== "After (JWT)" + + ```python + # JWT authentication example + # First, get a token + token_response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/auth/token", + auth=("username", "password"), + json={ + "ttl": 3600, + "scopes": ["messages:send"] + } + ) + + if token_response.status_code == 201: + token_data = token_response.json() + access_token = token_data["access_token"] + + # Then use the token + response = requests.post( + "https://api.sms-gate.app/3rdparty/v1/messages", + headers={ + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + }, + json={ + "phoneNumbers": ["+1234567890"], + "textMessage": {"text": "Hello world!"} + } + ) + ``` + +## Error Handling ⚠️ + +### Error Handling Best Practices + +1. **Check Token Expiration**: Implement token refresh before expiration +2. **Handle 401 Errors**: Re-authenticate and get a new token +3. **Handle 403 Errors**: Verify your token has the required scopes +4. **Log Errors**: Record authentication errors for debugging + +### Error Handling Example (Python) + +```python +import requests +import time +from datetime import datetime, timedelta + +class SMSGatewayClient: + def __init__(self, gateway_url, username, password): + self.gateway_url = gateway_url + self.username = username + self.password = password + self.access_token = None + self.token_expires_at = None + + def get_token(self, scopes, ttl=3600): + """Get a new JWT token""" + response = requests.post( + f"{self.gateway_url}/3rdparty/v1/auth/token", + auth=(self.username, self.password), + headers={"Content-Type": "application/json"}, + json={"ttl": ttl, "scopes": scopes} + ) + + if response.status_code == 201: + token_data = response.json() + self.access_token = token_data["access_token"] + # Parse expiration time + self.token_expires_at = datetime.fromisoformat( + token_data["expires_at"].replace("Z", "+00:00") + ) + return self.access_token + else: + raise Exception(f"Failed to get token: {response.status_code} - {response.text}") + + def ensure_valid_token(self, scopes): + """Ensure we have a valid token, refresh if needed""" + if (self.access_token is None or + self.token_expires_at is None or + datetime.now() + timedelta(minutes=5) >= self.token_expires_at): + self.get_token(scopes) + return self.access_token + + def send_message(self, recipient, message): + """Send a message with automatic token handling""" + try: + token = self.ensure_valid_token(["messages:send"]) + + response = requests.post( + f"{self.gateway_url}/3rdparty/v1/messages", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + }, + json={"recipient": recipient, "textMessage": {"text": message}} + ) + + if response.status_code == 401: + # Token might be expired or invalid, try once more + token = self.get_token(["messages:send"]) + response = requests.post( + f"{self.gateway_url}/3rdparty/v1/messages", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + }, + json={"recipient": recipient, "textMessage": {"text": message}} + ) + + if response.status_code == 202: + return response.json() + else: + raise Exception(f"Failed to send message: {response.status_code} - {response.text}") + + except Exception as e: + print(f"Error sending message: {str(e)}") + raise + +# Usage example +client = SMSGatewayClient("https://api.sms-gate.app", "username", "password") +result = client.send_message("+1234567890", "Hello from JWT!") +print("Message sent:", result) +``` + +## Security Considerations πŸ”’ + +### Token Security + +- **Keep Tokens Secret**: Treat JWT tokens like passwords +- **Use HTTPS**: Always transmit tokens over encrypted connections +- **Short Expiration**: Use the shortest practical TTL for your use case +- **Scope Limitation**: Request only the scopes you need +- **Secure Storage**: Store tokens securely on the server side, not in client-side code +- **Revocation**: Implement token revocation for compromised tokens + +### Client Security + +- **Validate Tokens**: Always validate server responses +- **Error Handling**: Implement proper error handling for authentication failures +- **Token Storage**: Store tokens securely (e.g., HttpOnly cookies, secure server-side storage) +- **CSRF Protection**: Implement CSRF protection for web applications + +## See Also πŸ”— + +- [API Reference](api.md) - Complete API endpoint documentation +- [Integration Guide](index.md) - Overview of integration options +- [Client Libraries](client-libraries.md) - Pre-built libraries for various languages +- [Authentication FAQ](../faq/authentication.md) - Frequently Asked Questions about JWT authentication diff --git a/docs/integration/client-libraries.md b/docs/integration/client-libraries.md index ea63ffe..77c006d 100644 --- a/docs/integration/client-libraries.md +++ b/docs/integration/client-libraries.md @@ -21,3 +21,10 @@ We offer client libraries in various programming languages to assist with integr [:material-github: GitHub Repo](https://github.com/android-sms-gateway/client-py) + +## Support πŸ“ž + +For issues or questions about the client libraries: + +- **GitHub Issues**: Report bugs or request features on the respective repository +- **Documentation**: Refer to the README files in each repository for detailed usage instructions diff --git a/mkdocs.yml b/mkdocs.yml index 86bea70..40467d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ nav: - Integration: - Overview: integration/index.md - API: integration/api.md + - Authentication: integration/authentication.md - Libraries: integration/client-libraries.md - CLI: integration/cli.md - GET to POST: integration/get-to-post.md @@ -75,6 +76,7 @@ nav: - Webhooks: faq/webhooks.md - Reading Messages: faq/reading-messages.md - Errors: faq/errors.md + - Authentication: faq/authentication.md - Resources: - Examples: resources/examples.md - Third-Party: resources/3rdparty.md From eb4763266ff8b94f8f1f0f13b1b2aaef984c8fc6 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 10 Dec 2025 07:01:13 +0700 Subject: [PATCH 2/2] [blog] add JWT blog post --- .../blog/jwt-authentication-migration.png | Bin 0 -> 87838 bytes docs/blog/index.md | 2 + ...2025-12-09_jwt-authentication-migration.md | 647 ++++++++++++++++++ 3 files changed, 649 insertions(+) create mode 100644 docs/assets/blog/jwt-authentication-migration.png create mode 100644 docs/blog/posts/2025-12-09_jwt-authentication-migration.md diff --git a/docs/assets/blog/jwt-authentication-migration.png b/docs/assets/blog/jwt-authentication-migration.png new file mode 100644 index 0000000000000000000000000000000000000000..42c213d5a873d8ee50249f2c5017c9bd6fb7f220 GIT binary patch literal 87838 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 literal 0 HcmV?d00001 diff --git a/docs/blog/index.md b/docs/blog/index.md index 577d7fd..3d59d61 100644 --- a/docs/blog/index.md +++ b/docs/blog/index.md @@ -5,10 +5,12 @@ Welcome to the SMSGate blog! Here you'll find the latest news, updates, and tech ## Categories - [**API**](/blog/category/api/): Technical documentation and integration guides for RESTful API endpoints +- [**Authentication**](/blog/category/authentication/): Strategies and techniques for secure user authentication and authorization - [**Best Practices**](/blog/category/best-practices/): Recommended approaches and optimization techniques for common scenarios - [**Documentation**](/blog/category/documentation/): Comprehensive reference materials and detailed platform guides - [**Features**](/blog/category/features/): In-depth explorations of platform capabilities and advanced functionality - [**IoT**](/blog/category/iot/): Use cases and implementation strategies for Internet of Things applications +- [**Security**](/blog/category/security/): Tips and best practices for secure communication and data protection - [**Tutorials**](/blog/category/tutorials/): Step-by-step implementation guides for specific workflows --- diff --git a/docs/blog/posts/2025-12-09_jwt-authentication-migration.md b/docs/blog/posts/2025-12-09_jwt-authentication-migration.md new file mode 100644 index 0000000..59f05a7 --- /dev/null +++ b/docs/blog/posts/2025-12-09_jwt-authentication-migration.md @@ -0,0 +1,647 @@ +--- +title: "Securing Your SMS Gateway: Migrating from Basic Auth to JWT" +date: 2025-12-10 +categories: + - Security + - Authentication + - API +description: "Learn how JWT authentication enhances security, enables fine-grained access control, and improves performance over Basic Authentication in the SMS Gateway API. Complete migration guide with code examples." +author: SMSGate Team LLM / Claude Sonnet 4.5 +--- +# πŸ” Securing Your SMS Gateway: Migrating from Basic Auth to JWT + +Picture this: Your SMS gateway credentials get accidentally committed to a public GitHub repository. With Basic Authentication, every single API request transmits those credentials, creating countless opportunities for interception. One leaked password means immediate exposure of your entire SMS infrastructure. This scenario isn't hypotheticalβ€”it happens regularly in production environments, leading to security breaches, and unauthorized access. Modern API security demands a better approach. + +Enter JWT (JSON Web Token) authenticationβ€”a token-based authentication mechanism that eliminates the need to transmit credentials with every request while providing fine-grained access control through scopes. In this comprehensive guide, we'll explore why JWT authentication is replacing Basic Auth as the primary authentication method for the SMSGate API, walk through the technical implementation, and provide complete code examples for a smooth migration. Whether you're maintaining existing integrations or building new ones, understanding this transition is essential for securing your SMS infrastructure. + + + +
+ JWT Authentication Migration +
+ +## 🎯 Why JWT Authentication? + +### The Basic Auth Problem + +Basic Authentication has served the web well for decades, but it suffers from fundamental limitations in modern API architectures: + +1. **Credential Transmission**: Username and password are sent with every single request (base64 encoded, but not encrypted) +2. **All-or-Nothing Access**: No way to limit what actions a credential can perform +3. **No Expiration**: Credentials remain valid indefinitely unless manually changed +4. **Difficult to Revoke**: Revoking access requires password changes across all systems +5. **Security Risk**: Credentials exposed in logs, network traces, or compromised systems grant full access + +### JWT Authentication Benefits + +JWT authentication addresses these concerns with a modern, secure approach: + +| Feature | Basic Auth | JWT Authentication | +| -------------------- | ------------------------------------- | ---------------------------------- | +| **Security** | Medium (credentials in every request) | High (token-based with expiration) | +| **Access Control** | All-or-nothing | Fine-grained via scopes | +| **Token Management** | None | Revocation, TTL, refresh | +| **Audit Trail** | Limited | Comprehensive (scopes, expiry) | +| **Recommended For** | Legacy systems only | All new integrations | + +!!! success "Key Advantages" + - **Enhanced Security**: Tokens expire automatically, limiting exposure window + - **Least Privilege**: Request only the permissions you need via scopes + - **Flexible Revocation**: Invalidate specific tokens without affecting others + - **Future-Proof**: Industry-standard approach with broad tooling support + +## πŸ”‘ Understanding JWT Tokens + +### Token Structure + +A JWT token consists of three base64-encoded parts separated by dots: + +```text +Header.Payload.Signature +``` + +**Example Token:** +```text +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwic2NvcGVzIjpbIm1lc3NhZ2VzOnNlbmQiXSwiZXhwIjoxNzMzNzg1MjAwfQ.signature_here +``` + +**Decoded Payload:** +```json +{ + "sub": "user123", + "scopes": ["messages:send", "messages:read"], + "exp": 1733785200, + "iat": 1733781600 +} +``` + +### JWT Scopes + +Scopes implement the principle of least privilege, allowing you to limit what each token can do: + +| Scope | Permission | Use Case | +| ---------------------------------------------------- | -------------------- | -------------------- | +| `messages:send` | Send SMS messages | Frontend application | +| `messages:list`, `messages:read` | Read message history | Analytics dashboard | +| `webhooks:list`, `webhooks:write`, `webhooks:delete` | Manage webhooks | Configuration panel | + +!!! tip "Scope Best Practice" + Always request the minimum scopes necessary. A token for sending messages doesn't need webhook management permissions. + +## πŸš€ Getting Started with JWT + +### Step 1: Generate Your First Token + +To generate a JWT token, make a POST request to the token endpoint using your existing Basic Auth credentials: + +=== "Python" + ```python + import requests + import json + + # Your Basic Auth credentials + USERNAME = "your_username" + PASSWORD = "your_password" + + # Token endpoint + token_url = "https://api.sms-gate.app/3rdparty/v1/auth/token" + + # Token configuration + token_request = { + "ttl": 3600, # Token validity in seconds (1 hour) + "scopes": ["messages:send", "messages:read"] + } + + response = requests.post( + token_url, + auth=(USERNAME, PASSWORD), + headers={"Content-Type": "application/json"}, + json=token_request + ) + + if response.status_code == 201: + token_data = response.json() + access_token = token_data["access_token"] + expires_at = token_data["expires_at"] + + print(f"βœ“ Token generated successfully") + print(f"Token: {access_token[:50]}...") + print(f"Expires: {expires_at}") + else: + print(f"βœ— Error: {response.status_code}") + print(response.text) + ``` + +=== "JavaScript" + ```javascript + const axios = require('axios'); + + // Your Basic Auth credentials + const USERNAME = 'your_username'; + const PASSWORD = 'your_password'; + + // Token endpoint + const tokenUrl = 'https://api.sms-gate.app/3rdparty/v1/auth/token'; + + // Token configuration + const tokenRequest = { + ttl: 3600, // Token validity in seconds (1 hour) + scopes: ['messages:send', 'messages:read'] + }; + + axios.post(tokenUrl, tokenRequest, { + auth: { username: USERNAME, password: PASSWORD }, + headers: { 'Content-Type': 'application/json' } + }) + .then(response => { + const { access_token, expires_at } = response.data; + console.log('βœ“ Token generated successfully'); + console.log(`Token: ${access_token.substring(0, 50)}...`); + console.log(`Expires: ${expires_at}`); + }) + .catch(error => { + console.error(`βœ— Error: ${error.response?.status}`); + console.error(error.response?.data); + }); + ``` + +=== "cURL" + ```bash + curl -X POST "https://api.sms-gate.app/3rdparty/v1/auth/token" \ + -u "username:password" \ + -H "Content-Type: application/json" \ + -d '{ + "ttl": 3600, + "scopes": ["messages:send", "messages:read"] + }' + ``` + +**Response:** +```json +{ + "id": "nHDAWaPS6zv3itRUpM9ko", + "token_type": "Bearer", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": "2025-12-10T03:03:09Z" +} +``` + +### Step 2: Use the JWT Token + +Once you have a token, include it in the `Authorization` header of your API requests: + +=== "Python" + ```python + import requests + + # Your JWT token + access_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + + # Send SMS with JWT + send_url = "https://api.sms-gate.app/3rdparty/v1/messages" + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + message_data = { + "phoneNumbers": ["+1234567890"], + "textMessage": {"text": "Hello from JWT!"} + } + + response = requests.post(send_url, headers=headers, json=message_data) + + if response.status_code == 200: + print("βœ“ Message sent successfully") + print(response.json()) + else: + print(f"βœ— Error: {response.status_code}") + ``` + +=== "JavaScript" + ```javascript + const axios = require('axios'); + + // Your JWT token + const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; + + // Send SMS with JWT + const sendUrl = 'https://api.sms-gate.app/3rdparty/v1/messages'; + const headers = { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }; + + const messageData = { + phoneNumbers: ['+1234567890'], + textMessage: { text: 'Hello from JWT!' } + }; + + axios.post(sendUrl, messageData, { headers }) + .then(response => { + console.log('βœ“ Message sent successfully'); + console.log(response.data); + }) + .catch(error => { + console.error(`βœ— Error: ${error.response?.status}`); + }); + ``` + +=== "cURL" + ```bash + curl -X POST "https://api.sms-gate.app/3rdparty/v1/messages" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "phoneNumbers": ["+1234567890"], + "textMessage": {"text": "Hello from JWT!"} + }' + ``` + +## πŸ”„ Migration Strategy + +### Phase 1: Dual Authentication Support + +Both Basic Auth and JWT will be supported during the transition period, allowing for gradual migration: + +```python +class SMSGatewayClient: + """Client supporting both Basic Auth and JWT""" + + def __init__(self, base_url, username, password): + self.base_url = base_url + self.username = username + self.password = password + self.access_token = None + self.token_expires_at = None + + def _get_headers(self, use_jwt=True): + """Get appropriate headers based on auth method""" + headers = {"Content-Type": "application/json"} + + if use_jwt and self.access_token: + headers["Authorization"] = f"Bearer {self.access_token}" + + return headers + + def _get_auth(self, use_jwt=True): + """Get auth tuple for Basic Auth""" + if use_jwt: + return None # JWT uses header + return (self.username, self.password) + + def send_message(self, phone_numbers, text, use_jwt=True): + """Send SMS using JWT (default) or Basic Auth""" + response = requests.post( + f"{self.base_url}/messages", + headers=self._get_headers(use_jwt), + auth=self._get_auth(use_jwt), + json={ + "phoneNumbers": phone_numbers, + "textMessage": {"text": text} + } + ) + return response +``` + +### Phase 2: Implement Token Management + +Implement proper token lifecycle management: + +```python +from datetime import datetime, timedelta +import requests + +class JWTTokenManager: + """Manages JWT token lifecycle""" + + def __init__(self, token_url, username, password): + self.token_url = token_url + self.username = username + self.password = password + self.access_token = None + self.expires_at = None + + def get_token(self, scopes, ttl=3600): + """Get a new JWT token""" + response = requests.post( + self.token_url, + auth=(self.username, self.password), + json={"ttl": ttl, "scopes": scopes} + ) + + if response.status_code == 201: + data = response.json() + self.access_token = data["access_token"] + self.expires_at = datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ) + return self.access_token + else: + raise Exception(f"Token generation failed: {response.text}") + + def is_valid(self): + """Check if current token is still valid""" + if not self.access_token or not self.expires_at: + return False + + # Add 60 second buffer before expiration + return datetime.now(self.expires_at.tzinfo) < ( + self.expires_at - timedelta(seconds=60) + ) + + def ensure_valid_token(self, scopes): + """Ensure we have a valid token, refresh if needed""" + if not self.is_valid(): + return self.get_token(scopes) + return self.access_token + +# Usage +token_manager = JWTTokenManager( + "https://api.sms-gate.app/3rdparty/v1/auth/token", + "username", + "password" +) + +# Always get a valid token +token = token_manager.ensure_valid_token(["messages:send"]) +``` + +### Phase 3: Full JWT Migration + +Complete the migration by removing Basic Auth fallbacks: + +```python +import requests +from datetime import datetime + +class SMSGatewayJWT: + """JWT-only SMS Gateway client""" + + def __init__(self, base_url, username, password): + self.base_url = base_url + self.token_manager = JWTTokenManager( + f"{base_url}/auth/token", + username, + password + ) + + def _make_request(self, method, endpoint, scopes, **kwargs): + """Make authenticated request with automatic token refresh""" + token = self.token_manager.ensure_valid_token(scopes) + + headers = kwargs.pop("headers", {}) + headers["Authorization"] = f"Bearer {token}" + + response = requests.request( + method, + f"{self.base_url}{endpoint}", + headers=headers, + **kwargs + ) + return response + + def send_message(self, phone_numbers, text): + """Send SMS message""" + return self._make_request( + "POST", + "/messages", + scopes=["messages:send"], + json={ + "phoneNumbers": phone_numbers, + "textMessage": {"text": text} + } + ) + + def get_messages(self, limit=50, offset=0): + """Retrieve message history""" + return self._make_request( + "GET", + "/messages", + scopes=["messages:read"], + params={"limit": limit, "offset": offset} + ) +``` + +## πŸ›‘οΈ Security Best Practices + +### 1. Token Storage + +Never store tokens in client-side code or version control: + +```python +import os + +# βœ“ Good: Environment variables +TOKEN_URL = os.getenv("SMS_TOKEN_URL") +USERNAME = os.getenv("SMS_USERNAME") +PASSWORD = os.getenv("SMS_PASSWORD") + +# βœ— Bad: Hardcoded credentials +TOKEN_URL = "https://api.sms-gate.app/3rdparty/v1/auth/token" +USERNAME = "my_username" # Never do this! +PASSWORD = "my_password" # Never do this! +``` + +### 2. Minimal Token TTL + +Use the shortest practical token lifetime: + +```python +# For long-running services +token_manager.get_token(scopes=["messages:send"], ttl=3600) # 1 hour + +# For batch jobs +token_manager.get_token(scopes=["messages:send"], ttl=600) # 10 minutes + +# For one-time operations +token_manager.get_token(scopes=["messages:send"], ttl=300) # 5 minutes +``` + +### 3. Scope Limitation + +Request only necessary scopes: + +```python +# βœ“ Good: Minimal scopes +send_token = get_token(scopes=["messages:send"]) +read_token = get_token(scopes=["messages:read"]) + +# βœ— Bad: Excessive permissions +admin_token = get_token(scopes=["all:any"]) +``` + +### 4. Token Revocation + +Revoke tokens when no longer needed: + +```python +def revoke_token(token, jti): + """Revoke a JWT token""" + response = requests.delete( + f"https://api.sms-gate.app/3rdparty/v1/auth/token/{jti}", + headers={"Authorization": f"Bearer {token}"} + ) + return response.status_code == 204 +``` + +## 🎯 Common Use Cases + +### 1. Frontend Application + +Generate short-lived tokens with limited scopes: + +```javascript +// Token for sending messages only (1 hour) +const frontendToken = await generateToken({ + scopes: ['messages:send'], + ttl: 3600 +}); +``` + +### 2. Analytics Dashboard + +Read-only access to message history: + +```python +# Token for analytics (24 hours) +analytics_token = generate_token( + scopes=["messages:list", "messages:read"], + ttl=86400 +) +``` + +### 3. Admin Tools + +Full access with moderate expiration: + +```python +# Token for administration (4 hours) +admin_token = generate_token( + scopes=["messages:list", "messages:read", "devices:list", "devices:delete", "webhooks:list", "webhooks:write", "webhooks:delete"], + ttl=14400 +) +``` + +### 4. Automated Jobs + +Minimal permissions for batch operations: + +```python +# Token for nightly report generation (1 hour) +batch_token = generate_token( + scopes=["messages:list"], + ttl=3600 +) +``` + +## ⚠️ Troubleshooting + +### Invalid Token Error + +**Problem**: Getting 401 "invalid token" errors + +**Solutions**: +1. Verify token hasn't expired +2. Check authorization header format (should start with "Bearer ") +3. Ensure token was generated successfully +4. Verify server time synchronization + +```python +# Debug token validation +from datetime import datetime +import jwt + +try: + # Decode without verification to inspect + decoded = jwt.decode(token, options={"verify_signature": False}) + exp = datetime.fromtimestamp(decoded['exp']) + + if datetime.now() > exp: + print("βœ— Token expired") + else: + print(f"βœ“ Token valid until {exp}") +except Exception as e: + print(f"βœ— Invalid token format: {e}") +``` + +### Insufficient Permissions + +**Problem**: Getting 403 "forbidden" errors + +**Solution**: Verify token has required scopes + +```python +# Check token scopes +decoded = jwt.decode(token, options={"verify_signature": False}) +scopes = decoded.get('scopes', []) + +required_scope = "messages:send" +if required_scope in scopes: + print(f"βœ“ Token has {required_scope}") +else: + print(f"βœ— Token missing {required_scope}") + print(f"Available scopes: {scopes}") +``` + +## πŸŽ“ Migration Checklist + +Use this checklist for a smooth transition: + +- [ ] **Week 1: Preparation** + - [ ] Review JWT documentation + - [ ] Test token generation in development + - [ ] Identify all services using Basic Auth + - [ ] Plan scope requirements per service + +- [ ] **Week 2: Implementation** + - [ ] Implement token management class + - [ ] Add JWT support to existing clients + - [ ] Create dual-auth fallback mechanism + - [ ] Set up monitoring for auth errors + +- [ ] **Week 3: Testing** + - [ ] Test in staging environment + - [ ] Verify all scopes work correctly + - [ ] Load test JWT performance + - [ ] Document token refresh flows + +- [ ] **Week 4: Deployment** + - [ ] Deploy JWT support to production + - [ ] Monitor error rates + - [ ] Gradually shift traffic to JWT + - [ ] Keep Basic Auth as fallback + +- [ ] **Week 5+: Cleanup** + - [ ] Verify 100% JWT usage + - [ ] Remove Basic Auth code + - [ ] Update all documentation + - [ ] Archive Basic Auth credentials + +## πŸŽ‰ Conclusion + +JWT authentication represents a significant security and performance upgrade over Basic Authentication. By implementing token-based authentication with fine-grained scopes, you gain: + +- **Enhanced security** through time-limited, revocable tokens +- **Fine-grained access control** with scopes +- **Improved auditability** and monitoring +- **Industry-standard** approach with broad tooling support + +The migration process is straightforward with the dual-authentication support during transition. Start by generating your first JWT token today, test it alongside Basic Auth, and gradually migrate your services. The security and performance benefits are well worth the effort. + +Ready to get started? Check out our [Authentication Guide](../../integration/authentication.md) for complete API documentation, or explore our [client libraries](../../integration/client-libraries.md) with built-in JWT support. + +Have questions about JWT migration? Join the discussion on [GitHub](https://github.com/capcom6/android-sms-gateway/discussions) and share your experience with the community! + +## πŸ”— Related Resources + +- [Authentication Guide](../../integration/authentication.md) - Complete JWT documentation +- [Authentication FAQ](../../faq/authentication.md) - Common questions and answers +- [API Reference](../../integration/api.md) - Full API documentation +- [Client Libraries](../../integration/client-libraries.md) - Pre-built JWT integration + +## πŸ“š Related Posts + +- [Mastering Message Retrieval: A Developer's Guide to GET /messages API](./2025-08-07_get-messages-api-guide.md) +- [Targeting Messages to Specific Devices](./2025-07-20_targeting-messages-to-specific-devices.md) +- [Beyond Plain Text: Unlocking the Hidden Power of Data SMS](./2025-07-12_beyond-plain-text-unlocking-data-sms.md) \ No newline at end of file