Compare commits
3 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
471f4153ce | |
|
|
da921f870a | |
|
|
145566ad7d |
|
|
@ -0,0 +1,333 @@
|
|||
# AWS ElastiCache for Redis Setup Guide
|
||||
|
||||
This guide walks you through setting up Redis on AWS using ElastiCache, which is AWS's managed Redis service.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS Account with appropriate permissions
|
||||
- AWS CLI configured (optional, but helpful)
|
||||
- Your application running on AWS (EC2, ECS, Lambda, etc.)
|
||||
- VPC (Virtual Private Cloud) set up in AWS
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### Step 1: Create a VPC and Subnet Group (if not already done)
|
||||
|
||||
ElastiCache requires a VPC. If you don't have one:
|
||||
|
||||
1. **Go to AWS Console → VPC Dashboard**
|
||||
2. **Create VPC** (if needed):
|
||||
- Click "Create VPC"
|
||||
- Choose "VPC and more"
|
||||
- Name: `your-app-vpc`
|
||||
- IPv4 CIDR: `10.0.0.0/16`
|
||||
- Create public and private subnets
|
||||
- Enable DNS hostnames
|
||||
|
||||
3. **Create Subnet Group for ElastiCache**:
|
||||
- Go to **ElastiCache → Subnet Groups**
|
||||
- Click "Create subnet group"
|
||||
- Name: `redis-subnet-group`
|
||||
- VPC: Select your VPC
|
||||
- Availability Zones: Select at least 2 zones
|
||||
- Subnets: Select private subnets (recommended for security)
|
||||
|
||||
### Step 2: Create Security Group
|
||||
|
||||
1. **Go to EC2 → Security Groups**
|
||||
2. **Create Security Group**:
|
||||
- Name: `redis-security-group`
|
||||
- Description: `Security group for ElastiCache Redis`
|
||||
- VPC: Select your VPC
|
||||
- **Inbound Rules**: Add rule
|
||||
- Type: `Custom TCP`
|
||||
- Port: `6379`
|
||||
- Source: Select your application's security group (or specific IP/CIDR)
|
||||
- **Outbound Rules**: Default (All traffic)
|
||||
|
||||
### Step 3: Create ElastiCache Redis Cluster
|
||||
|
||||
1. **Go to AWS Console → ElastiCache**
|
||||
2. **Click "Create" → Choose "Redis"**
|
||||
3. **Configure Cluster Settings**:
|
||||
|
||||
**Cluster Settings**:
|
||||
- **Name**: `your-app-redis` (must be unique)
|
||||
- **Description**: `Redis cache for rate limiting and OTP`
|
||||
- **Engine version**: Latest Redis 7.x (recommended)
|
||||
- **Port**: `6379` (default)
|
||||
- **Parameter group**: `default.redis7` (or create custom)
|
||||
|
||||
**Node Type**:
|
||||
- **Node type**: Choose based on your needs:
|
||||
- `cache.t3.micro` - Free tier eligible, 0.5 GB RAM
|
||||
- `cache.t3.small` - 1.37 GB RAM (~$15/month)
|
||||
- `cache.t3.medium` - 3.09 GB RAM (~$30/month)
|
||||
- For production: `cache.t4g.medium` or larger
|
||||
|
||||
**Network & Security**:
|
||||
- **VPC**: Select your VPC
|
||||
- **Subnet group**: Select the subnet group created in Step 1
|
||||
- **Availability Zone(s)**:
|
||||
- Single AZ (cheaper, less resilient)
|
||||
- Multi-AZ (recommended for production, automatic failover)
|
||||
- **Security groups**: Select `redis-security-group` created in Step 2
|
||||
|
||||
**Backup & Maintenance**:
|
||||
- **Automatic backups**: Enable (recommended)
|
||||
- **Backup retention**: 1-7 days (your choice)
|
||||
- **Backup window**: Choose low-traffic time
|
||||
- **Maintenance window**: Choose low-traffic time
|
||||
|
||||
**Encryption**:
|
||||
- **Encryption in-transit**: Enable (recommended for production)
|
||||
- **Encryption at-rest**: Enable (recommended for production)
|
||||
- **Auth token**:
|
||||
- **Enable**: Recommended for production
|
||||
- **Auth token**: Generate a strong password (save this!)
|
||||
|
||||
4. **Review and Create**
|
||||
- Review all settings
|
||||
- Click "Create"
|
||||
|
||||
### Step 4: Wait for Cluster Creation
|
||||
|
||||
- ElastiCache takes 5-15 minutes to create
|
||||
- Status will change from "creating" to "available"
|
||||
- Note the **Primary Endpoint** (e.g., `your-app-redis.xxxxx.cache.amazonaws.com:6379`)
|
||||
|
||||
### Step 5: Configure Your Application
|
||||
|
||||
Update your `.env` file with the ElastiCache endpoint:
|
||||
|
||||
#### Option A: Without Auth Token (Not Recommended for Production)
|
||||
|
||||
```env
|
||||
REDIS_URL=redis://your-app-redis.xxxxx.cache.amazonaws.com:6379
|
||||
```
|
||||
|
||||
#### Option B: With Auth Token (Recommended)
|
||||
|
||||
```env
|
||||
REDIS_URL=redis://:your-auth-token@your-app-redis.xxxxx.cache.amazonaws.com:6379
|
||||
```
|
||||
|
||||
#### Option C: Using Separate Variables
|
||||
|
||||
```env
|
||||
REDIS_HOST=your-app-redis.xxxxx.cache.amazonaws.com
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-auth-token
|
||||
```
|
||||
|
||||
#### Option D: With SSL/TLS (If Encryption in-transit is enabled)
|
||||
|
||||
```env
|
||||
REDIS_URL=rediss://:your-auth-token@your-app-redis.xxxxx.cache.amazonaws.com:6379
|
||||
```
|
||||
|
||||
**Note**: `rediss://` (with double 's') indicates SSL/TLS connection.
|
||||
|
||||
### Step 6: Update Security Group (If Needed)
|
||||
|
||||
If your application can't connect:
|
||||
|
||||
1. **Check Security Group Rules**:
|
||||
- Ensure your application's security group can access port 6379
|
||||
- Or add your application's security group ID to Redis security group inbound rules
|
||||
|
||||
2. **Test Connection** (from EC2 instance):
|
||||
```bash
|
||||
# Install redis-cli
|
||||
sudo yum install redis -y # Amazon Linux
|
||||
# or
|
||||
sudo apt-get install redis-tools -y # Ubuntu
|
||||
|
||||
# Test connection
|
||||
redis-cli -h your-app-redis.xxxxx.cache.amazonaws.com -p 6379 -a your-auth-token ping
|
||||
# Should return: PONG
|
||||
```
|
||||
|
||||
### Step 7: Verify Connection
|
||||
|
||||
1. **Restart your application**
|
||||
2. **Check logs** for:
|
||||
```
|
||||
✅ Redis Client: Ready
|
||||
```
|
||||
3. **If you see errors**, check:
|
||||
- Security group rules
|
||||
- VPC routing
|
||||
- Auth token is correct
|
||||
- Endpoint URL is correct
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
### Free Tier
|
||||
- AWS Free Tier includes 750 hours/month of `cache.t2.micro` or `cache.t3.micro`
|
||||
- Perfect for development/testing
|
||||
|
||||
### Cost-Saving Tips
|
||||
1. **Use smaller instance types** for development
|
||||
2. **Disable automatic backups** for non-production
|
||||
3. **Use single-AZ** for development (multi-AZ costs more)
|
||||
4. **Stop/Delete** clusters when not in use
|
||||
5. **Reserved Instances** for production (save up to 55%)
|
||||
|
||||
## High Availability Setup
|
||||
|
||||
### Multi-AZ Configuration
|
||||
1. **Enable Multi-AZ** during cluster creation
|
||||
2. **Automatic failover** if primary node fails
|
||||
3. **Read Replicas** for read scaling:
|
||||
- Go to ElastiCache → Your cluster → Actions → Add replica
|
||||
- Choose availability zones
|
||||
- Replicas can be promoted to primary if needed
|
||||
|
||||
### Cluster Mode (Redis Cluster)
|
||||
For larger scale:
|
||||
1. **Enable Cluster Mode** during creation
|
||||
2. **Multiple shards** for horizontal scaling
|
||||
3. **Automatic sharding** across nodes
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **✅ Enable Auth Token** (password authentication)
|
||||
2. **✅ Enable Encryption in-transit** (SSL/TLS)
|
||||
3. **✅ Enable Encryption at-rest**
|
||||
4. **✅ Use Private Subnets** (not public subnets)
|
||||
5. **✅ Restrict Security Groups** (only allow your application)
|
||||
6. **✅ Use VPC Endpoints** (if accessing from Lambda)
|
||||
7. **✅ Regular Security Updates** (AWS handles this)
|
||||
|
||||
## Monitoring and Alerts
|
||||
|
||||
### CloudWatch Metrics
|
||||
1. **Go to ElastiCache → Your Cluster → Monitoring**
|
||||
2. **Key Metrics to Monitor**:
|
||||
- `CPUUtilization` - Should be < 80%
|
||||
- `MemoryUtilization` - Should be < 80%
|
||||
- `NetworkBytesIn/Out` - Network traffic
|
||||
- `CacheHits/CacheMisses` - Cache performance
|
||||
- `Evictions` - Memory pressure indicator
|
||||
|
||||
### Set Up Alarms
|
||||
1. **Go to CloudWatch → Alarms**
|
||||
2. **Create alarms for**:
|
||||
- High CPU (> 80%)
|
||||
- High Memory (> 80%)
|
||||
- Connection failures
|
||||
- Failover events
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Timeout
|
||||
|
||||
**Problem**: Application can't connect to Redis
|
||||
|
||||
**Solutions**:
|
||||
1. Check security group allows traffic from your application
|
||||
2. Verify VPC routing tables
|
||||
3. Ensure both are in same VPC
|
||||
4. Check if endpoint URL is correct
|
||||
5. Verify auth token is correct
|
||||
|
||||
### Authentication Failed
|
||||
|
||||
**Problem**: `NOAUTH Authentication required` or `WRONGPASS`
|
||||
|
||||
**Solutions**:
|
||||
1. Verify auth token in `.env` matches ElastiCache auth token
|
||||
2. Check if auth token is enabled in ElastiCache
|
||||
3. Use format: `redis://:password@host:port`
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Problem**: Memory utilization > 90%
|
||||
|
||||
**Solutions**:
|
||||
1. Upgrade to larger node type
|
||||
2. Enable eviction policy (already enabled by default)
|
||||
3. Review what data is stored in Redis
|
||||
4. Set TTL on keys (your code already does this)
|
||||
|
||||
### Slow Performance
|
||||
|
||||
**Problem**: High latency or slow responses
|
||||
|
||||
**Solutions**:
|
||||
1. Check CPU utilization
|
||||
2. Enable read replicas for read-heavy workloads
|
||||
3. Upgrade node type
|
||||
4. Check network latency (use same region as application)
|
||||
5. Monitor `CacheHits` vs `CacheMisses` ratio
|
||||
|
||||
## Alternative: AWS MemoryDB for Redis
|
||||
|
||||
For even higher durability (data persisted to disk):
|
||||
|
||||
1. **Go to MemoryDB** (separate service)
|
||||
2. **Similar setup** to ElastiCache
|
||||
3. **Better durability** (multi-AZ with automatic failover)
|
||||
4. **Higher cost** than ElastiCache
|
||||
5. **Use when**: Data persistence is critical
|
||||
|
||||
## Alternative: Self-Hosted on EC2
|
||||
|
||||
If you prefer more control:
|
||||
|
||||
1. **Launch EC2 instance** (Amazon Linux or Ubuntu)
|
||||
2. **Install Redis**:
|
||||
```bash
|
||||
sudo yum install redis -y # Amazon Linux
|
||||
sudo systemctl start redis
|
||||
sudo systemctl enable redis
|
||||
```
|
||||
3. **Configure security group** (port 6379)
|
||||
4. **Set up Redis password** in `/etc/redis.conf`
|
||||
5. **Use EC2 private IP** in your `.env`
|
||||
|
||||
**Note**: You'll need to manage backups, updates, and scaling yourself.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Get Endpoint URL
|
||||
```bash
|
||||
aws elasticache describe-cache-clusters \
|
||||
--cache-cluster-id your-app-redis \
|
||||
--show-cache-node-info \
|
||||
--query 'CacheClusters[0].CacheNodes[0].Endpoint.Address'
|
||||
```
|
||||
|
||||
### Get Auth Token
|
||||
- Go to ElastiCache → Your Cluster → Configuration
|
||||
- Auth token is shown (or set during creation)
|
||||
|
||||
### Update Auth Token
|
||||
1. Go to ElastiCache → Your Cluster
|
||||
2. Actions → Modify
|
||||
3. Change auth token
|
||||
4. Apply immediately or schedule
|
||||
|
||||
### Delete Cluster
|
||||
1. Go to ElastiCache → Your Cluster
|
||||
2. Actions → Delete
|
||||
3. Confirm deletion
|
||||
4. **Warning**: This deletes all data!
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Create ElastiCache Redis cluster
|
||||
2. ✅ Update `.env` with endpoint and auth token
|
||||
3. ✅ Update security groups
|
||||
4. ✅ Restart application
|
||||
5. ✅ Verify connection: `✅ Redis Client: Ready`
|
||||
6. ✅ Set up CloudWatch alarms
|
||||
7. ✅ Monitor performance
|
||||
|
||||
## Support
|
||||
|
||||
- **AWS Documentation**: https://docs.aws.amazon.com/elasticache/
|
||||
- **AWS Support**: AWS Console → Support Center
|
||||
- **Pricing Calculator**: https://calculator.aws/
|
||||
|
||||
|
|
@ -53,10 +53,12 @@ REDIS_URL=redis://localhost:6379
|
|||
### Option 3: Use Cloud Redis (Production)
|
||||
|
||||
For production, use a managed Redis service:
|
||||
- **AWS ElastiCache**: `redis://your-elasticache-endpoint:6379`
|
||||
- **AWS ElastiCache**: `redis://your-elasticache-endpoint:6379` - [📖 Complete AWS Setup Guide](./AWS_REDIS_SETUP.md)
|
||||
- **Redis Cloud**: `redis://user:password@redis-cloud-host:6379`
|
||||
- **Azure Cache for Redis**: `redis://your-cache.redis.cache.windows.net:6380?ssl=true`
|
||||
|
||||
**For AWS deployments, see the detailed guide**: [AWS Redis Setup Guide](./AWS_REDIS_SETUP.md)
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```env
|
||||
|
|
|
|||
|
|
@ -2206,12 +2206,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.0.0]
|
||||
## [3.2.3]
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require
|
||||
that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key)
|
||||
when using HMAC algorithms.
|
||||
- Upgrading JWA version to 1.4.2, adressing a compatibility issue for Node >= 25.
|
||||
|
||||
## [3.0.0]
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING**: `jwt.verify` now requires an `algorithm` parameter, and
|
||||
`jws.createVerify` requires an `algorithm` option. The `"alg"` field
|
||||
signature headers is ignored. This mitigates a critical security flaw
|
||||
|
|
@ -12,7 +24,9 @@ All notable changes to this project will be documented in this file.
|
|||
for details.
|
||||
|
||||
## [2.0.0] - 2015-01-30
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING**: Default payload encoding changed from `binary` to
|
||||
`utf8`. `utf8` is a is a more sensible default than `binary` because
|
||||
many payloads, as far as I can tell, will contain user-facing
|
||||
|
|
@ -21,14 +35,13 @@ All notable changes to this project will be documented in this file.
|
|||
- Code reorganization, thanks [@fearphage]! (<code>[7880050]</code>)
|
||||
|
||||
### Added
|
||||
|
||||
- Option in all relevant methods for `encoding`. For those few users
|
||||
that might be depending on a `binary` encoding of the messages, this
|
||||
is for them. (<code>[6b6de48]</code>)
|
||||
|
||||
[unreleased]: https://github.com/brianloveswords/node-jws/compare/v2.0.0...HEAD
|
||||
[2.0.0]: https://github.com/brianloveswords/node-jws/compare/v1.0.1...v2.0.0
|
||||
|
||||
[7880050]: https://github.com/brianloveswords/node-jws/commit/7880050
|
||||
[6b6de48]: https://github.com/brianloveswords/node-jws/commit/6b6de48
|
||||
|
||||
[@fearphage]: https://github.com/fearphage
|
||||
|
|
|
|||
|
|
@ -34,7 +34,12 @@ function jwsSign(opts) {
|
|||
}
|
||||
|
||||
function SignStream(opts) {
|
||||
var secret = opts.secret||opts.privateKey||opts.key;
|
||||
var secret = opts.secret;
|
||||
secret = secret == null ? opts.privateKey : secret;
|
||||
secret = secret == null ? opts.key : secret;
|
||||
if (/^hs/i.test(opts.header.alg) === true && secret == null) {
|
||||
throw new TypeError('secret must be a string or buffer or a KeyObject')
|
||||
}
|
||||
var secretStream = new DataStream(secret);
|
||||
this.readable = true;
|
||||
this.header = opts.header;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,12 @@ function jwsDecode(jwsSig, opts) {
|
|||
|
||||
function VerifyStream(opts) {
|
||||
opts = opts || {};
|
||||
var secretOrKey = opts.secret||opts.publicKey||opts.key;
|
||||
var secretOrKey = opts.secret;
|
||||
secretOrKey = secretOrKey == null ? opts.publicKey : secretOrKey;
|
||||
secretOrKey = secretOrKey == null ? opts.key : secretOrKey;
|
||||
if (/^hs/i.test(opts.algorithm) === true && secretOrKey == null) {
|
||||
throw new TypeError('secret must be a string or buffer or a KeyObject')
|
||||
}
|
||||
var secretStream = new DataStream(secretOrKey);
|
||||
this.readable = true;
|
||||
this.algorithm = opts.algorithm;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jws",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"description": "Implementation of JSON Web Signatures",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"readmeFilename": "readme.md",
|
||||
"gitHead": "c0f6b27bcea5a2ad2e304d91c2e842e4076a6b03",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -2240,12 +2240,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1184,4 +1184,190 @@ router.post(
|
|||
}
|
||||
);
|
||||
|
||||
// POST /auth/validate-token
|
||||
// Endpoint for other services (like BuySellService) to validate JWT tokens
|
||||
// This allows centralized token validation
|
||||
router.post('/validate-token', async (req, res) => {
|
||||
const clientIp = req.ip || req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || 'unknown';
|
||||
const userAgent = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
console.log(`[Auth Service] POST /auth/validate-token - Request received`);
|
||||
console.log(`[Auth Service] Client IP: ${clientIp}`);
|
||||
console.log(`[Auth Service] User-Agent: ${userAgent.substring(0, 80)}`);
|
||||
console.log(`[Auth Service] Request body contains token: ${!!req.body?.token}`);
|
||||
|
||||
try {
|
||||
const { token } = req.body;
|
||||
|
||||
if (!token) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Token is missing in request body`);
|
||||
return res.status(400).json({
|
||||
valid: false,
|
||||
error: 'Token is required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Auth Service] Token received (length: ${token.length} characters)`);
|
||||
|
||||
// Use the auth middleware logic to validate token
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getKeySecret, getAllKeys, validateTokenClaims } = require('../services/jwtKeys');
|
||||
const db = require('../db');
|
||||
|
||||
// Decode token to get key ID from header
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.decode(token, { complete: true });
|
||||
if (!decoded || !decoded.header) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Invalid token format - unable to decode header`);
|
||||
return res.json({ valid: false, error: 'Invalid token format' });
|
||||
}
|
||||
console.log(`[Auth Service] Token decoded successfully`);
|
||||
console.log(`[Auth Service] Token header:`, {
|
||||
alg: decoded.header.alg,
|
||||
kid: decoded.header.kid || 'NOT SET',
|
||||
typ: decoded.header.typ,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Error decoding token - ${err.message}`);
|
||||
return res.json({ valid: false, error: 'Invalid token' });
|
||||
}
|
||||
|
||||
// Get secret for the key ID (if present) or try all keys
|
||||
const keyId = decoded.header.kid;
|
||||
let payload = null;
|
||||
let verified = false;
|
||||
let verificationMethod = null;
|
||||
|
||||
if (keyId) {
|
||||
console.log(`[Auth Service] Key ID found in token: ${keyId}`);
|
||||
const secret = getKeySecret(keyId);
|
||||
if (secret) {
|
||||
console.log(`[Auth Service] Attempting verification with key ID: ${keyId}`);
|
||||
try {
|
||||
payload = jwt.verify(token, secret);
|
||||
verified = true;
|
||||
verificationMethod = `keyId: ${keyId}`;
|
||||
console.log(`[Auth Service] ✅ Token verified successfully using key ID: ${keyId}`);
|
||||
} catch (err) {
|
||||
console.log(`[Auth Service] ❌ Verification failed with key ID ${keyId}: ${err.message}`);
|
||||
return res.json({ valid: false, error: 'Invalid or expired token' });
|
||||
}
|
||||
} else {
|
||||
console.log(`[Auth Service] ⚠️ Key ID ${keyId} not found in available keys, will try all keys`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[Auth Service] ⚠️ No key ID in token header, trying all available keys`);
|
||||
}
|
||||
|
||||
// If key ID not found or not specified, try all keys (for rotation support)
|
||||
if (!verified) {
|
||||
const allKeys = getAllKeys();
|
||||
console.log(`[Auth Service] Attempting verification with ${Object.keys(allKeys).length} available keys`);
|
||||
for (const [kid, keySecret] of Object.entries(allKeys)) {
|
||||
try {
|
||||
payload = jwt.verify(token, keySecret);
|
||||
verified = true;
|
||||
verificationMethod = `allKeys: ${kid}`;
|
||||
console.log(`[Auth Service] ✅ Token verified successfully using key: ${kid}`);
|
||||
break;
|
||||
} catch (err) {
|
||||
// Try next key silently
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!verified) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Token verification failed with all available keys`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!verified || !payload) {
|
||||
console.log(`[Auth Service] ❌ FINAL RESULT: Token is invalid or expired`);
|
||||
return res.json({ valid: false, error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
console.log(`[Auth Service] Token payload extracted:`, {
|
||||
userId: payload.sub,
|
||||
role: payload.role,
|
||||
userType: payload.user_type,
|
||||
exp: payload.exp ? new Date(payload.exp * 1000).toISOString() : 'NOT SET',
|
||||
iat: payload.iat ? new Date(payload.iat * 1000).toISOString() : 'NOT SET',
|
||||
tokenVersion: payload.token_version || 1,
|
||||
});
|
||||
|
||||
// Validate JWT claims (iss, aud, exp, iat, nbf)
|
||||
console.log(`[Auth Service] Validating JWT claims...`);
|
||||
const claimsValidation = validateTokenClaims(payload);
|
||||
if (!claimsValidation.valid) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Claims validation failed - ${claimsValidation.reason}`);
|
||||
return res.json({ valid: false, error: 'Invalid token claims' });
|
||||
}
|
||||
console.log(`[Auth Service] ✅ Claims validation passed`);
|
||||
|
||||
// Validate token_version to ensure token hasn't been invalidated by logout-all-devices
|
||||
console.log(`[Auth Service] Checking token version in database for user: ${payload.sub}`);
|
||||
try {
|
||||
const { rows } = await db.query(
|
||||
`SELECT COALESCE(token_version, 1) as token_version FROM users WHERE id = $1`,
|
||||
[payload.sub]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log(`[Auth Service] ❌ FAILED: User not found in database: ${payload.sub}`);
|
||||
return res.json({ valid: false, error: 'User not found' });
|
||||
}
|
||||
|
||||
const userTokenVersion = rows[0].token_version;
|
||||
const tokenVersion = payload.token_version || 1;
|
||||
|
||||
console.log(`[Auth Service] Token version check:`, {
|
||||
tokenVersion,
|
||||
userTokenVersion,
|
||||
match: tokenVersion === userTokenVersion,
|
||||
});
|
||||
|
||||
// If token version doesn't match, token has been invalidated
|
||||
if (tokenVersion !== userTokenVersion) {
|
||||
console.log(`[Auth Service] ❌ FAILED: Token version mismatch - token has been invalidated`);
|
||||
return res.json({ valid: false, error: 'Token has been invalidated' });
|
||||
}
|
||||
console.log(`[Auth Service] ✅ Token version check passed`);
|
||||
} catch (dbErr) {
|
||||
// Handle missing token_version column gracefully
|
||||
if (dbErr.code === '42703' && dbErr.message && dbErr.message.includes('token_version')) {
|
||||
console.warn('[Auth Service] ⚠️ token_version column not found in database, skipping version check');
|
||||
// Continue without token version validation
|
||||
} else {
|
||||
console.error('[Auth Service] ❌ Error validating token version:', dbErr);
|
||||
return res.status(500).json({ valid: false, error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Token is valid - return user info
|
||||
console.log(`[Auth Service] ✅ FINAL RESULT: Token is VALID`);
|
||||
console.log(`[Auth Service] Returning user info:`, {
|
||||
userId: payload.sub,
|
||||
role: payload.role,
|
||||
userType: payload.user_type,
|
||||
verificationMethod,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
valid: true,
|
||||
payload: {
|
||||
sub: payload.sub,
|
||||
role: payload.role,
|
||||
user_type: payload.user_type,
|
||||
phone_number: payload.phone_number,
|
||||
token_version: payload.token_version || 1,
|
||||
high_assurance: payload.high_assurance || false,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Auth Service] ❌ UNEXPECTED ERROR in validate-token:', err);
|
||||
console.error('[Auth Service] Error stack:', err.stack);
|
||||
return res.status(500).json({ valid: false, error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
Loading…
Reference in New Issue