Files
2025-10-26 02:25:24 -07:00

849 lines
21 KiB
Markdown

# Authentication & Authorization Guide
This guide explains how to use the authentication and authorization features in the Notifier service.
## Overview
The Notifier service includes:
- **API Key Authentication**: Simple token-based authentication using Bearer tokens or API keys
- **Role-Based Access Control (RBAC)**: Fine-grained authorization for specific notifiers
- **Rate Limiting**: Per-key request rate limiting to prevent abuse
- **Audit Logging**: All auth failures and API key usage are logged
## Enabling Authentication
Authentication is **disabled by default**. To enable it, you need:
1. **PostgreSQL database** for persistent key storage
2. **Auth configuration** in your config file
### Configuration
Add to your configuration file:
```yaml
auth:
enabled: true
default_rate_limit: 100 # requests per minute, 0 = unlimited
database:
url: "postgresql://user:password@localhost:5432/notifier"
```
Or via environment variables:
```bash
export NOTIFIER_AUTH_ENABLED=true
export NOTIFIER_AUTH_DEFAULT_RATE_LIMIT=100
export NOTIFIER_AUTH_DATABASE_URL="postgresql://user:password@localhost:5432/notifier"
```
### Database Setup
The database schema is automatically created on first connection. You only need to:
1. Create a PostgreSQL database (e.g., `notifier`)
2. Provide database URL in configuration
3. The service will create required tables:
- `api_keys` - Stores API key metadata
- `api_key_audit_log` - Tracks all key operations
**Example**: Creating a PostgreSQL database
```bash
createdb notifier
# Or via SQL:
# CREATE DATABASE notifier;
```
## Creating API Keys
API keys are created via the REST API once you have an admin key. The system uses a hybrid architecture with persistent PostgreSQL storage and in-memory cache for performance.
### Step 1: Bootstrap (Initial Setup)
On first deployment, create an initial admin key via environment variables:
```bash
export NOTIFIER_AUTH_ENABLED=true
export NOTIFIER_BOOTSTRAP_ADMIN_KEY=true
export NOTIFIER_AUTH_DATABASE_URL="postgresql://user:password@localhost:5432/notifier"
./notifier serve
# Output:
# ============================================================
# NOTIFIER BOOTSTRAP: ADMIN KEY CREATED
# ============================================================
# Key: nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# Save this key in a secure location.
# ============================================================
```
**Important**: Save the admin key securely. You won't be able to see it again.
### Step 2: Create Additional Keys
Use the admin key to create keys for your services via REST API:
```bash
ADMIN_KEY="nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
# Create a key for your billing service
curl -X POST http://localhost:8080/api/v1/admin/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": "billing-service",
"roles": ["notify-email", "notify-slack"],
"rate_limit": 1000,
"expires_in": "8760h"
}'
```
**Response**:
```json
{
"key": "nk_b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1",
"name": "billing-service-1698297600",
"client_id": "billing-service",
"roles": ["notify-email", "notify-slack"],
"created_at": "2024-10-26T12:00:00Z",
"rate_limit": 1000
}
```
### Step 3: Store Key Securely
Store the returned key in a secure location:
```bash
echo "$BILLING_KEY" > ~/.billing-notifier-key
chmod 600 ~/.billing-notifier-key
```
### Database Persistence
Keys are automatically persisted to PostgreSQL database specified in configuration:
```yaml
auth:
enabled: true
database:
url: "postgresql://user:password@localhost:5432/notifier"
```
The system automatically creates the required schema:
- `api_keys` table - Stores key metadata
- `api_key_audit_log` table - Tracks all key operations
### Programmatic Creation (Go)
If you need to create keys programmatically in Go code:
```go
package main
import (
"context"
"fmt"
"time"
"github.com/igodwin/notifier/internal/auth"
)
func main() {
// Create database backend
dbStore, err := auth.NewKeyStoreDB("postgresql://user:password@localhost:5432/notifier")
if err != nil {
panic(err)
}
defer dbStore.Close()
// Create hybrid key store (memory cache + database backend)
cache := auth.NewAPIKeyStore()
keyStore := auth.NewHybridKeyStore(cache, dbStore)
// Load existing keys from database
ctx := context.Background()
if err := keyStore.InitializeFromDatabase(ctx); err != nil {
panic(err)
}
// Create an API key for a client
expiresIn := 30 * 24 * time.Hour // 30 days
key, err := keyStore.CreateKey(
ctx,
"billing-service", // Client ID
[]string{"notify-email", "notify-slack"}, // Roles
1000, // Rate limit: 1000 req/min
&expiresIn, // Expires in 30 days
"admin", // Who created it
)
if err != nil {
panic(err)
}
fmt.Printf("API Key: %s\n", key.Key)
fmt.Printf("Client ID: %s\n", key.ClientID)
fmt.Printf("Roles: %v\n", key.Roles)
fmt.Printf("Rate Limit: %d req/min\n", key.RateLimit)
fmt.Printf("Expires At: %v\n", key.ExpiresAt)
// Example output:
// API Key: nk_b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1
// Client ID: billing-service
// Roles: [notify-email notify-slack]
// Rate Limit: 1000 req/min
// Expires At: 2025-11-24 10:30:00 +0000 UTC
}
```
### Managing API Keys
Once created, you can list, revoke, and audit keys via REST API:
#### List Your Keys
```bash
curl -X GET http://localhost:8080/api/v1/admin/keys \
-H "Authorization: Bearer $YOUR_KEY"
```
**Response**:
```json
{
"keys": [
{
"key_preview": "nk_o5p6",
"name": "billing-service-1698297600",
"client_id": "billing-service",
"roles": ["notify-email", "notify-slack"],
"created_at": "2024-10-26T12:00:00Z",
"last_used_at": "2024-10-26T15:30:00Z",
"expires_at": "2025-10-26T12:00:00Z",
"is_active": true,
"rate_limit": 1000
}
]
}
```
Note: Only the last 4 characters of keys are shown for security.
#### Revoke a Key
```bash
curl -X DELETE http://localhost:8080/api/v1/admin/keys/nk_key_to_revoke \
-H "Authorization: Bearer $ADMIN_KEY"
```
Returns: `204 No Content` on success.
#### View Audit Log
```bash
curl -X GET http://localhost:8080/api/v1/admin/keys/nk_key/audit \
-H "Authorization: Bearer $ADMIN_KEY"
```
**Response**:
```json
{
"key_preview": "nk_o5p6",
"audit_log": [
{
"action": "created",
"performed_by": "admin-bootstrap",
"performed_at": "2024-10-26T12:00:00Z",
"details": {
"client_id": "billing-service",
"roles": ["notify-email", "notify-slack"]
}
},
{
"action": "deactivated",
"performed_by": "admin-user",
"performed_at": "2024-10-26T14:30:00Z"
}
]
}
```
### Key Naming Convention
Generated API keys follow the format: `nk_<32-hex-characters>`
- `nk_` prefix identifies it as a Notifier API key
- The hex string is cryptographically secure random
- Keys use cryptographically secure random number generation
### Key Properties
| Property | Description |
|----------|-------------|
| `Key` | The actual API key to use in requests |
| `ClientID` | Identifier for the client/service using the key |
| `Roles` | List of roles granted to this key (e.g., "notify-email", "notify-slack") |
| `RateLimit` | Requests per minute allowed (0 = unlimited) |
| `ExpiresAt` | Optional expiration date (if set, key becomes invalid after this time) |
| `CreatedAt` | Timestamp when the key was created |
| `LastUsedAt` | Timestamp of the last successful authentication |
| `IsActive` | Whether the key is currently active (can be deactivated) |
## API Key Roles
Roles control which notifiers a client can use. Common role patterns:
| Role | Purpose |
|------|---------|
| `notify-email` | Can use email (SMTP) notifiers |
| `notify-slack` | Can use Slack notifiers |
| `notify-ntfy` | Can use ntfy.sh notifiers |
| `notify-all` | Can use all notification types |
| `admin` | Full access (optional, for admin operations) |
You define your own roles based on your needs.
## Configuring Role-Based Access
Control which roles can use specific notifiers in your config:
```yaml
notifiers:
smtp:
default:
host: "smtp.example.com"
port: 587
username: "user@example.com"
password: "${SMTP_PASSWORD}"
from: "noreply@example.com"
use_tls: true
allowed_roles: # Empty list = all authenticated users can use
- "notify-email"
- "admin"
internal:
host: "smtp-internal.example.com"
port: 587
username: "internal@example.com"
password: "${SMTP_INTERNAL_PASSWORD}"
from: "internal@example.com"
use_tls: true
allowed_roles:
- "admin" # Only admins can use internal SMTP
slack:
default:
webhook_url: "${SLACK_WEBHOOK}"
username: "Notifier"
allowed_roles:
- "notify-slack"
- "notify-all"
ntfy:
default:
server_url: "https://ntfy.sh"
token: "${NTFY_TOKEN}"
allowed_roles:
- "notify-all"
```
## Using API Keys
### REST API
Include the API key in the `Authorization` header as a Bearer token:
```bash
curl -X POST http://localhost:8080/api/v1/notifications \
-H "Authorization: Bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"subject": "Hello",
"body": "World",
"recipients": ["user@example.com"]
}'
```
Alternatively, use the `X-API-Key` header:
```bash
curl -X POST http://localhost:8080/api/v1/notifications \
-H "X-API-Key: nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Content-Type: application/json" \
-d '{ ... }'
```
### gRPC
Include the API key in gRPC metadata:
```go
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
pb "github.com/igodwin/notifier/api/grpc/pb"
)
func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
// Create context with API key
ctx := context.Background()
md := metadata.New(map[string][]string{
"authorization": {"bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"},
})
ctx = metadata.NewOutgoingContext(ctx, md)
// Use the client
client := pb.NewNotifierServiceClient(conn)
resp, err := client.SendNotification(ctx, &pb.SendNotificationRequest{
Type: pb.NotificationType_NOTIFICATION_TYPE_EMAIL,
Subject: "Hello",
Body: "World",
Recipients: []string{"user@example.com"},
})
// ...
}
```
Or use `grpcurl`:
```bash
grpcurl -plaintext \
-H "authorization: bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-d '{"type":"NOTIFICATION_TYPE_EMAIL","subject":"Hello","body":"World","recipients":["user@example.com"]}' \
localhost:50051 notifier.v1.NotifierService/SendNotification
```
## Credential Management Best Practices
### For Self-Created Clients
**DO:**
- ✅ Store API keys in environment variables
- ✅ Store API keys in secure configuration management (Vault, AWS Secrets Manager)
- ✅ Rotate keys periodically (every 90 days recommended)
- ✅ Use separate keys per environment (dev, staging, prod)
- ✅ Use separate keys per service/application
- ✅ Monitor key usage via logs and audit trails
- ✅ Set expiration times on keys
- ✅ Use appropriate rate limits
**DON'T:**
- ❌ Store API keys in code or version control
- ❌ Include API keys in Docker images or build artifacts
- ❌ Log or display API keys in error messages
- ❌ Use wildcard roles like "admin" for non-admin services
- ❌ Share API keys between services
- ❌ Use the same key for multiple environments
### Example: Storing in Environment Variables
```bash
# .env file (not committed to git)
NOTIFIER_API_KEY="nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
```
```go
// In your application
import "os"
apiKey := os.Getenv("NOTIFIER_API_KEY")
```
### Example: Using with Configuration Management (Vault)
```go
package main
import (
"fmt"
"os"
vault "github.com/hashicorp/vault/api"
)
func getAPIKeyFromVault() (string, error) {
client, err := vault.NewClient(&vault.Config{
Address: os.Getenv("VAULT_ADDR"),
})
if err != nil {
return "", err
}
secret, err := client.Logical().Read("secret/data/notifier/api-key")
if err != nil {
return "", err
}
data := secret.Data["data"].(map[string]interface{})
return data["key"].(string), nil
}
```
## Client Implementation Examples
### Go Client
```go
package main
import (
"context"
"fmt"
"os"
"github.com/igodwin/notifier/api/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
type NotifierClient struct {
client pb.NotifierServiceClient
apiKey string
}
func NewNotifierClient(addr, apiKey string) (*NotifierClient, error) {
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
return &NotifierClient{
client: pb.NewNotifierServiceClient(conn),
apiKey: apiKey,
}, nil
}
func (nc *NotifierClient) SendNotification(ctx context.Context, req *pb.SendNotificationRequest) (*pb.SendNotificationResponse, error) {
// Add API key to context metadata
md := metadata.New(map[string][]string{
"authorization": {fmt.Sprintf("bearer %s", nc.apiKey)},
})
ctx = metadata.NewOutgoingContext(ctx, md)
return nc.client.SendNotification(ctx, req)
}
func main() {
apiKey := os.Getenv("NOTIFIER_API_KEY")
client, err := NewNotifierClient("localhost:50051", apiKey)
if err != nil {
panic(err)
}
resp, err := client.SendNotification(context.Background(), &pb.SendNotificationRequest{
Type: pb.NotificationType_NOTIFICATION_TYPE_EMAIL,
Subject: "Hello",
Body: "World",
Recipients: []string{"user@example.com"},
})
if err != nil {
panic(err)
}
fmt.Printf("Notification sent: %s\n", resp.Result.NotificationId)
}
```
### Python Client
```python
import os
import grpc
from notifier.api.grpc import notifier_pb2, notifier_pb2_grpc
def send_notification(subject, body, recipients):
api_key = os.getenv("NOTIFIER_API_KEY")
# Create secure channel
channel = grpc.secure_channel("localhost:50051", grpc.ssl_channel_credentials())
stub = notifier_pb2_grpc.NotifierServiceStub(channel)
# Create metadata with API key
metadata = [("authorization", f"bearer {api_key}")]
# Send notification
request = notifier_pb2.SendNotificationRequest(
type=notifier_pb2.NOTIFICATION_TYPE_EMAIL,
subject=subject,
body=body,
recipients=recipients,
)
response = stub.SendNotification(request, metadata=metadata)
return response.result.notification_id
if __name__ == "__main__":
notif_id = send_notification(
"Hello",
"World",
["user@example.com"]
)
print(f"Notification sent: {notif_id}")
```
### Node.js/TypeScript Client
```typescript
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import * as os from "os";
const NOTIFIER_API_KEY = os.getenv("NOTIFIER_API_KEY");
const packageDef = protoLoader.loadSync("notifier.proto", {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const notifierProto = grpc.loadPackageDefinition(packageDef);
async function sendNotification(subject: string, body: string, recipients: string[]) {
// Create metadata with API key
const metadata = new grpc.Metadata();
metadata.set("authorization", `bearer ${NOTIFIER_API_KEY}`);
// Create client
const client = new (notifierProto.notifier.v1.NotifierService as any)(
"localhost:50051",
grpc.credentials.createInsecure()
);
return new Promise((resolve, reject) => {
client.sendNotification(
{
type: "NOTIFICATION_TYPE_EMAIL",
subject,
body,
recipients,
},
metadata,
(err: any, response: any) => {
if (err) reject(err);
else resolve(response.result.notification_id);
}
);
});
}
// Usage
sendNotification("Hello", "World", ["user@example.com"])
.then((notifId) => console.log(`Notification sent: ${notifId}`))
.catch((err) => console.error(err));
```
### cURL Examples
```bash
# Send email notification
curl -X POST http://localhost:8080/api/v1/notifications \
-H "Authorization: Bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"subject": "Alert",
"body": "Something happened",
"recipients": ["admin@example.com"]
}'
# Batch notifications
curl -X POST http://localhost:8080/api/v1/notifications/batch \
-H "Authorization: Bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Content-Type: application/json" \
-d '{
"notifications": [
{
"type": "email",
"subject": "Alert 1",
"body": "First alert",
"recipients": ["user1@example.com"]
},
{
"type": "slack",
"subject": "Alert 2",
"body": "Second alert",
"recipients": ["#alerts"]
}
]
}'
# Get notification status
curl -X GET http://localhost:8080/api/v1/notifications/{id} \
-H "Authorization: Bearer nk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
```
## Error Responses
### Authentication Failures
**REST API:**
```
401 Unauthorized
Missing or invalid Authorization header
403 Forbidden
Rate limit exceeded
401 Unauthorized
Invalid API key
401 Unauthorized
API key has expired
```
**gRPC:**
```
UNAUTHENTICATED: Missing or invalid Authorization header
UNAUTHENTICATED: Invalid API key
UNAUTHENTICATED: API key has expired
RESOURCE_EXHAUSTED: Rate limit exceeded
PERMISSION_DENIED: Insufficient permissions for this notifier
```
## Configuration Examples
### Example 1: Multi-Tenant Setup
```yaml
auth:
enabled: true
default_rate_limit: 100
notifiers:
smtp:
default:
host: "smtp.example.com"
port: 587
username: "shared@example.com"
password: "${SMTP_PASSWORD}"
from: "notifications@example.com"
allowed_roles:
- "notify-all"
tenant-a:
host: "smtp.tenant-a.com"
port: 587
username: "notifications@tenant-a.com"
password: "${TENANT_A_SMTP_PASSWORD}"
from: "notifications@tenant-a.com"
allowed_roles:
- "tenant-a-notifications"
tenant-b:
host: "smtp.tenant-b.com"
port: 587
username: "notifications@tenant-b.com"
password: "${TENANT_B_SMTP_PASSWORD}"
from: "notifications@tenant-b.com"
allowed_roles:
- "tenant-b-notifications"
```
### Example 2: Restricted Access
```yaml
auth:
enabled: true
default_rate_limit: 50
notifiers:
smtp:
default:
host: "smtp.example.com"
port: 587
username: "user@example.com"
password: "${SMTP_PASSWORD}"
from: "noreply@example.com"
allowed_roles:
- "admin" # Only admins
- "email-service"
slack:
default:
webhook_url: "${SLACK_WEBHOOK}"
allowed_roles:
- "admin"
- "alerts" # Only alert systems
```
## Monitoring & Auditing
Authentication events are logged with the following information:
```json
{
"timestamp": "2025-10-25T10:30:00Z",
"event": "auth_success",
"client_id": "billing-service",
"roles": ["notify-email", "notify-slack"],
"rate_limit_remaining": 95,
"endpoint": "/api/v1/notifications"
}
```
Authentication failures are also logged for security auditing:
```json
{
"timestamp": "2025-10-25T10:31:00Z",
"event": "auth_failure",
"reason": "invalid_api_key",
"remote_addr": "192.168.1.100"
}
```
Monitor these logs for:
- Brute force attempts (multiple failed authentications from same IP)
- Unusual access patterns
- Rate limit violations
- Key expiration approaching
- Inactive keys being used
## Summary
1. **Enable auth** in config: `auth.enabled: true`
2. **Bootstrap admin key** on first deployment
3. **Create API keys** via REST API with appropriate roles and rate limits
4. **Configure role-based access** for each notifier
5. **Use environment variables** or secrets manager for key storage
6. **Monitor logs** and audit trails for security events
7. **Rotate keys regularly** and set expiration dates
8. **Use separate keys** for each service/application
## Related Documentation
For more detailed information on specific topics:
- **[KEY_MANAGEMENT.md](./KEY_MANAGEMENT.md)** - Complete guide to API key management
- Bootstrap mechanism
- Key creation via REST API
- Key listing, revocation, and rotation
- Audit logging
- Database persistence
- Kubernetes deployment
- **[RBAC.md](./RBAC.md)** - Role-Based Access Control (RBAC) guide
- Configuration patterns
- Authorization flow
- Restricting notifier access by role
- Testing authorization
- Security best practices
- **[RBAC_QUICKSTART.md](./RBAC_QUICKSTART.md)** - RBAC quick reference
- 60-second overview
- Common patterns
- Troubleshooting
- **[AUTH_QUICK_START.md](./AUTH_QUICK_START.md)** - Quick start guide
- Step-by-step setup
- Basic examples