test twenty
This commit is contained in:
parent
144409502b
commit
2de985e78f
3
.env
3
.env
@ -47,7 +47,8 @@ NEXT_PUBLIC_IFRAME_AGILITY_URL=https://agilite.slm-lab.net/oidc/login
|
||||
NEXT_PUBLIC_IFRAME_ARTLAB_URL=https://artlab.slm-lab.net
|
||||
NEXT_PUBLIC_IFRAME_GITE_URL=https://gite.slm-lab.net/user/oauth2/cube
|
||||
NEXT_PUBLIC_IFRAME_CALCULATION_URL=https://calcul.slm-lab.net
|
||||
NEXT_PUBLIC_IFRAME_MEDIATIONS_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?client_id=mediations.slm-lab.net&redirect_uri=https%3A%2F%2Fmediations.slm-lab.net%2F%3Fopenid_mode%3Dtrue&scope=openid%20profile%20email&response_type=code
|
||||
#NEXT_PUBLIC_IFRAME_MEDIATIONS_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?client_id=mediations.slm-lab.net&redirect_uri=https%3A%2F%2Fmediations.slm-lab.net%2F%3Fopenid_mode%3Dtrue&scope=openid%20profile%20email&response_type=code
|
||||
NEXT_PUBLIC_IFRAME_MEDIATIONS_URL=https://mediation.governance-labs.com
|
||||
NEXT_PUBLIC_IFRAME_SHOWCASE_URL=https://www.slm-lab.net
|
||||
NEXT_PUBLIC_IFRAME_CARNET_URL=https://journal.governance-labs.com/
|
||||
NEXT_PUBLIC_IFRAME_LIVRE_URL=https://memoire.slm-lab.net/
|
||||
|
||||
153
MICROSOFT_OAUTH_ANALYSIS.md
Normal file
153
MICROSOFT_OAUTH_ANALYSIS.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Microsoft OAuth Token Management Analysis
|
||||
|
||||
## Current Implementation
|
||||
|
||||
### Token Storage Locations
|
||||
|
||||
1. **Redis Cache** (Primary for OAuth tokens)
|
||||
- **Location**: `lib/redis.ts` → `cacheEmailCredentials()`
|
||||
- **TTL**: 24 hours (`TTL.CREDENTIALS = 60 * 60 * 24`)
|
||||
- **Stored**: `accessToken`, `refreshToken`, `tokenExpiry`, `useOAuth`
|
||||
- **Key Format**: `email:credentials:${userId}:${accountId}`
|
||||
|
||||
2. **Prisma Database** (Schema has fields but NOT used for OAuth tokens)
|
||||
- **Location**: `prisma/schema.prisma` → `MailCredentials` model
|
||||
- **Fields Available**: `refresh_token`, `access_token`, `token_expiry`, `use_oauth`
|
||||
- **Current Status**: ❌ **Tokens are NOT saved to Prisma** (only Redis)
|
||||
- **Code Comment**: "OAuth fields don't exist" (but they DO exist in schema!)
|
||||
|
||||
### Token Refresh Flow
|
||||
|
||||
**Location**: `lib/services/token-refresh.ts` → `ensureFreshToken()`
|
||||
|
||||
1. Checks Redis for credentials
|
||||
2. Validates token expiry (5-minute buffer)
|
||||
3. Refreshes token if needed via Microsoft API
|
||||
4. **Updates Redis only** (not Prisma)
|
||||
5. Returns new access token
|
||||
|
||||
### Issues Identified
|
||||
|
||||
#### 🔴 Critical Issue #1: Refresh Tokens Not Persisted to Database
|
||||
|
||||
**Problem**:
|
||||
- Refresh tokens are only stored in Redis with 24-hour TTL
|
||||
- If Redis is cleared, restarted, or TTL expires, refresh tokens are **permanently lost**
|
||||
- Microsoft refresh tokens can last up to **90 days** (or indefinitely with `offline_access` scope)
|
||||
- Users would need to re-authenticate if Redis data is lost
|
||||
|
||||
**Impact**:
|
||||
- ❌ Not viable for long-term production use
|
||||
- ❌ Data loss risk on Redis restarts
|
||||
- ❌ No backup/recovery mechanism
|
||||
|
||||
#### 🟡 Issue #2: Token Refresh Doesn't Update Database
|
||||
|
||||
**Problem**:
|
||||
- When tokens are refreshed, only Redis is updated
|
||||
- Prisma database still has old/expired tokens (if any)
|
||||
- Schema has the fields but they're never populated
|
||||
|
||||
**Impact**:
|
||||
- ⚠️ Inconsistency between Redis and Database
|
||||
- ⚠️ Can't recover from Redis cache loss
|
||||
|
||||
#### 🟡 Issue #3: Missing Refresh Token in Logs
|
||||
|
||||
From your logs:
|
||||
```
|
||||
hasRefreshToken: false
|
||||
```
|
||||
|
||||
This suggests the refresh token might not be properly saved or retrieved.
|
||||
|
||||
### Microsoft OAuth Token Lifespan
|
||||
|
||||
- **Access Token**: ~1 hour (3600 seconds)
|
||||
- **Refresh Token**: Up to 90 days (with `offline_access` scope)
|
||||
- **Token Refresh**: Returns new access token, may return new refresh token
|
||||
|
||||
### Required Scopes
|
||||
|
||||
Current implementation uses:
|
||||
```typescript
|
||||
const REQUIRED_SCOPES = [
|
||||
'offline_access', // ✅ Required for long-lived refresh tokens
|
||||
'https://outlook.office.com/IMAP.AccessAsUser.All',
|
||||
'https://outlook.office.com/SMTP.Send'
|
||||
].join(' ');
|
||||
```
|
||||
|
||||
✅ `offline_access` is included - this is correct for long-term use.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### ✅ Fix #1: Persist Refresh Tokens to Prisma
|
||||
|
||||
**Why**: Refresh tokens are critical for long-term access and should be persisted to database.
|
||||
|
||||
**Implementation**:
|
||||
1. Save `refresh_token` to Prisma `MailCredentials.refresh_token` field
|
||||
2. Update `token_expiry` when tokens are refreshed
|
||||
3. Keep access tokens in Redis (short-lived, can be regenerated)
|
||||
4. Use Prisma as source of truth for refresh tokens
|
||||
|
||||
### ✅ Fix #2: Update Database on Token Refresh
|
||||
|
||||
**Why**: Keep database in sync with refreshed tokens.
|
||||
|
||||
**Implementation**:
|
||||
1. After refreshing tokens, update Prisma `MailCredentials` record
|
||||
2. Update `access_token` and `token_expiry` fields
|
||||
3. Update `refresh_token` if Microsoft returns a new one
|
||||
|
||||
### ✅ Fix #3: Fallback to Database if Redis Missing
|
||||
|
||||
**Why**: Recover from Redis cache loss.
|
||||
|
||||
**Implementation**:
|
||||
1. If Redis cache is empty, check Prisma for refresh token
|
||||
2. Use Prisma refresh token to get new access token
|
||||
3. Re-populate Redis cache
|
||||
|
||||
## Long-Term Viability Assessment
|
||||
|
||||
### Current State: ⚠️ **NOT VIABLE** for long-term production
|
||||
|
||||
**Reasons**:
|
||||
1. ❌ Refresh tokens only in volatile Redis cache
|
||||
2. ❌ No persistence mechanism
|
||||
3. ❌ Risk of data loss on Redis restart
|
||||
4. ❌ No recovery mechanism
|
||||
|
||||
### After Fixes: ✅ **VIABLE** for long-term production
|
||||
|
||||
**With recommended fixes**:
|
||||
1. ✅ Refresh tokens persisted to database
|
||||
2. ✅ Redis used for fast access token retrieval
|
||||
3. ✅ Database as source of truth
|
||||
4. ✅ Recovery mechanism in place
|
||||
|
||||
## Token Storage Strategy (Recommended)
|
||||
|
||||
### Access Tokens
|
||||
- **Storage**: Redis (fast, short-lived)
|
||||
- **TTL**: 1 hour (matches Microsoft token expiry)
|
||||
- **Purpose**: Fast IMAP/SMTP authentication
|
||||
|
||||
### Refresh Tokens
|
||||
- **Storage**: Prisma Database (persistent, long-term)
|
||||
- **TTL**: None (stored indefinitely until revoked)
|
||||
- **Purpose**: Long-term access, token renewal
|
||||
|
||||
### Token Expiry
|
||||
- **Storage**: Both Redis and Prisma
|
||||
- **Purpose**: Know when to refresh tokens
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **HIGH**: Persist refresh tokens to Prisma
|
||||
2. **HIGH**: Update Prisma on token refresh
|
||||
3. **MEDIUM**: Add fallback to database if Redis missing
|
||||
4. **LOW**: Add token encryption at rest (if required by compliance)
|
||||
|
||||
130
MICROSOFT_OAUTH_FIXES.md
Normal file
130
MICROSOFT_OAUTH_FIXES.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Microsoft OAuth Token Management - Fixes Applied
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### ✅ Fix #1: Refresh Tokens Now Persisted to Prisma Database
|
||||
|
||||
**Problem**: Refresh tokens were only stored in Redis (24-hour TTL), risking permanent loss.
|
||||
|
||||
**Solution**:
|
||||
- Refresh tokens are now saved to `MailCredentials.refresh_token` in Prisma
|
||||
- Access tokens and expiry also persisted to database
|
||||
- Database acts as source of truth for long-term token storage
|
||||
|
||||
**Files Modified**:
|
||||
- `lib/services/email-service.ts` - `saveUserEmailCredentials()` now saves OAuth tokens to Prisma
|
||||
|
||||
### ✅ Fix #2: Database Updated on Token Refresh
|
||||
|
||||
**Problem**: When tokens were refreshed, only Redis was updated, leaving database stale.
|
||||
|
||||
**Solution**:
|
||||
- Token refresh now updates both Redis AND Prisma
|
||||
- New refresh tokens (if provided by Microsoft) are persisted
|
||||
- Token expiry timestamp updated in database
|
||||
|
||||
**Files Modified**:
|
||||
- `lib/services/token-refresh.ts` - `ensureFreshToken()` now updates Prisma after refresh
|
||||
|
||||
### ✅ Fix #3: Fallback to Database if Redis Missing
|
||||
|
||||
**Problem**: If Redis cache was empty, system couldn't recover refresh tokens.
|
||||
|
||||
**Solution**:
|
||||
- If Redis cache miss, system checks Prisma database
|
||||
- Retrieves refresh token from database
|
||||
- Re-populates Redis cache for future use
|
||||
|
||||
**Files Modified**:
|
||||
- `lib/services/token-refresh.ts` - Added database fallback logic
|
||||
|
||||
### ✅ Fix #4: OAuth Fields Retrieved from Database
|
||||
|
||||
**Problem**: When loading credentials from database, OAuth fields were ignored.
|
||||
|
||||
**Solution**:
|
||||
- Database queries now include OAuth fields (`access_token`, `refresh_token`, `token_expiry`, `use_oauth`)
|
||||
- Credentials object properly populated with OAuth data from database
|
||||
|
||||
**Files Modified**:
|
||||
- `lib/services/email-service.ts` - `getImapConnection()` now includes OAuth fields from database
|
||||
|
||||
## Token Storage Strategy (Current)
|
||||
|
||||
### Access Tokens
|
||||
- **Primary**: Redis (fast access, 24-hour TTL)
|
||||
- **Backup**: Prisma Database (persisted)
|
||||
- **Lifespan**: ~1 hour (Microsoft default)
|
||||
|
||||
### Refresh Tokens
|
||||
- **Primary**: Prisma Database (persistent, long-term)
|
||||
- **Cache**: Redis (24-hour TTL, for fast access)
|
||||
- **Lifespan**: Up to 90 days (with `offline_access` scope)
|
||||
|
||||
### Token Expiry
|
||||
- **Storage**: Both Redis and Prisma
|
||||
- **Purpose**: Determine when to refresh tokens
|
||||
|
||||
## Long-Term Viability
|
||||
|
||||
### ✅ NOW VIABLE for Production
|
||||
|
||||
**Improvements**:
|
||||
1. ✅ Refresh tokens persisted to database
|
||||
2. ✅ Database updated on token refresh
|
||||
3. ✅ Fallback mechanism if Redis fails
|
||||
4. ✅ No data loss on Redis restart
|
||||
5. ✅ Recovery mechanism in place
|
||||
|
||||
## What Happens Now
|
||||
|
||||
### When Adding Microsoft Account:
|
||||
1. OAuth tokens saved to **both** Redis and Prisma
|
||||
2. Refresh token stored in database for long-term access
|
||||
3. Access token cached in Redis for fast retrieval
|
||||
|
||||
### When Token Expires:
|
||||
1. System checks Redis first (fast path)
|
||||
2. If Redis miss, checks Prisma database (fallback)
|
||||
3. Uses refresh token to get new access token
|
||||
4. Updates **both** Redis and Prisma with new tokens
|
||||
5. Continues normal operation
|
||||
|
||||
### If Redis is Cleared:
|
||||
1. System detects Redis cache miss
|
||||
2. Retrieves refresh token from Prisma database
|
||||
3. Gets new access token using refresh token
|
||||
4. Re-populates Redis cache
|
||||
5. **No user action required** ✅
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. **Test Token Refresh**:
|
||||
- Wait for access token to expire (~1 hour)
|
||||
- Verify system automatically refreshes
|
||||
- Check both Redis and Prisma are updated
|
||||
|
||||
2. **Test Redis Failure**:
|
||||
- Clear Redis cache
|
||||
- Try to access email
|
||||
- Verify system recovers from database
|
||||
|
||||
3. **Test Long-Term Access**:
|
||||
- Wait several days
|
||||
- Verify refresh token still works
|
||||
- Check no re-authentication required
|
||||
|
||||
## Monitoring
|
||||
|
||||
Watch for these log messages:
|
||||
- ✅ `Token for ${email} persisted to Prisma database` - Token saved successfully
|
||||
- ✅ `Recovered credentials from Prisma and cached in Redis` - Fallback working
|
||||
- ⚠️ `Error persisting tokens to database` - Database update failed (check logs)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Monitor**: Watch logs for token refresh operations
|
||||
2. **Verify**: Check Prisma database has `refresh_token` values
|
||||
3. **Test**: Verify email access works after Redis restart
|
||||
4. **Optional**: Consider encrypting tokens at rest (if compliance requires)
|
||||
|
||||
@ -290,6 +290,7 @@ export async function getImapConnection(
|
||||
});
|
||||
|
||||
// Create our credentials object from database data
|
||||
// Include OAuth tokens from database if available
|
||||
credentials = {
|
||||
email: dbCredentials.email,
|
||||
password: dbCredentials.password || '',
|
||||
@ -300,7 +301,12 @@ export async function getImapConnection(
|
||||
smtp_port: dbCredentials.smtp_port || undefined,
|
||||
smtp_secure: dbCredentials.smtp_secure ?? false,
|
||||
display_name: dbCredentials.display_name || undefined,
|
||||
color: dbCredentials.color || undefined
|
||||
color: dbCredentials.color || undefined,
|
||||
// Include OAuth fields from database
|
||||
useOAuth: dbCredentials.use_oauth || false,
|
||||
accessToken: dbCredentials.access_token || undefined,
|
||||
refreshToken: dbCredentials.refresh_token || undefined,
|
||||
tokenExpiry: dbCredentials.token_expiry ? dbCredentials.token_expiry.getTime() : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@ -588,9 +594,9 @@ export async function saveUserEmailCredentials(
|
||||
tokenExpiry: extendedCreds.tokenExpiry
|
||||
};
|
||||
|
||||
// Extract only the fields that exist in the database schema
|
||||
// Based on the schema from 'npx prisma db pull', OAuth fields don't exist
|
||||
const dbCredentials = {
|
||||
// Extract fields for database schema
|
||||
// OAuth tokens are now persisted to Prisma for long-term storage
|
||||
const dbCredentials: any = {
|
||||
email: credentials.email,
|
||||
password: credentials.password ?? '', // Required field in the DB schema
|
||||
host: credentials.host,
|
||||
@ -600,7 +606,12 @@ export async function saveUserEmailCredentials(
|
||||
smtp_port: credentials.smtp_port || null,
|
||||
smtp_secure: credentials.smtp_secure ?? false,
|
||||
display_name: credentials.display_name || null,
|
||||
color: credentials.color || null
|
||||
color: credentials.color || null,
|
||||
// Persist OAuth tokens to database for long-term storage
|
||||
use_oauth: oauthData.useOAuth || false,
|
||||
refresh_token: oauthData.refreshToken || null,
|
||||
access_token: oauthData.accessToken || null,
|
||||
token_expiry: oauthData.tokenExpiry ? new Date(oauthData.tokenExpiry) : null
|
||||
};
|
||||
|
||||
try {
|
||||
@ -609,10 +620,11 @@ export async function saveUserEmailCredentials(
|
||||
password: dbCredentials.password ? '***' : null,
|
||||
});
|
||||
|
||||
console.log('OAuth data will be saved to Redis cache only:', {
|
||||
console.log('OAuth data will be saved to both Prisma and Redis:', {
|
||||
hasOAuth: !!oauthData.useOAuth,
|
||||
hasAccessToken: !!oauthData.accessToken,
|
||||
hasRefreshToken: !!oauthData.refreshToken
|
||||
hasRefreshToken: !!oauthData.refreshToken,
|
||||
tokenExpiry: oauthData.tokenExpiry ? new Date(oauthData.tokenExpiry).toISOString() : null
|
||||
});
|
||||
|
||||
// Save to database using the unique constraint on [userId, email]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { refreshAccessToken } from './microsoft-oauth';
|
||||
import { getRedisClient, KEYS } from '@/lib/redis';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
/**
|
||||
* Check if a token is expired or about to expire (within 5 minutes)
|
||||
@ -17,19 +18,48 @@ export async function ensureFreshToken(
|
||||
email: string
|
||||
): Promise<{ accessToken: string; success: boolean }> {
|
||||
try {
|
||||
// Use Redis to get the tokens (no database lookup needed)
|
||||
// Try Redis first (fast path)
|
||||
console.log(`Checking if token refresh is needed for ${email}`);
|
||||
const redis = getRedisClient();
|
||||
const key = KEYS.CREDENTIALS(userId, email);
|
||||
const credStr = await redis.get(key);
|
||||
let credStr = await redis.get(key);
|
||||
let creds: any = null;
|
||||
|
||||
if (!credStr) {
|
||||
console.log(`No credentials found in Redis for ${email}`);
|
||||
return { accessToken: '', success: false };
|
||||
if (credStr) {
|
||||
creds = JSON.parse(credStr);
|
||||
} else {
|
||||
// Redis cache miss - fallback to Prisma database
|
||||
console.log(`No credentials found in Redis for ${email}, checking Prisma database...`);
|
||||
const account = await prisma.mailCredentials.findFirst({
|
||||
where: {
|
||||
userId: userId,
|
||||
email: email,
|
||||
use_oauth: true
|
||||
}
|
||||
});
|
||||
|
||||
if (account && account.refresh_token) {
|
||||
// Reconstruct credentials from database
|
||||
creds = {
|
||||
useOAuth: true,
|
||||
refreshToken: account.refresh_token,
|
||||
accessToken: account.access_token || null,
|
||||
tokenExpiry: account.token_expiry ? account.token_expiry.getTime() : null,
|
||||
email: account.email,
|
||||
host: account.host,
|
||||
port: account.port,
|
||||
secure: account.secure
|
||||
};
|
||||
|
||||
// Re-populate Redis cache
|
||||
await redis.set(key, JSON.stringify(creds), 'EX', 86400);
|
||||
console.log(`Recovered credentials from Prisma and cached in Redis for ${email}`);
|
||||
} else {
|
||||
console.log(`No OAuth credentials found in database for ${email}`);
|
||||
return { accessToken: '', success: false };
|
||||
}
|
||||
}
|
||||
|
||||
const creds = JSON.parse(credStr);
|
||||
|
||||
// If not OAuth or missing refresh token, return failure
|
||||
if (!creds.useOAuth || !creds.refreshToken) {
|
||||
console.log(`Account ${email} is not using OAuth or missing refresh token`);
|
||||
@ -57,6 +87,35 @@ export async function ensureFreshToken(
|
||||
await redis.set(key, JSON.stringify(creds), 'EX', 86400); // 24 hours
|
||||
console.log(`Token for ${email} refreshed and cached in Redis`);
|
||||
|
||||
// CRITICAL: Also persist to Prisma database for long-term storage
|
||||
// This ensures refresh tokens survive Redis restarts/expiry
|
||||
try {
|
||||
const account = await prisma.mailCredentials.findFirst({
|
||||
where: {
|
||||
userId: userId,
|
||||
email: email
|
||||
}
|
||||
});
|
||||
|
||||
if (account) {
|
||||
await prisma.mailCredentials.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token || account.refresh_token, // Keep existing if not provided
|
||||
token_expiry: new Date(Date.now() + (tokens.expires_in * 1000)),
|
||||
use_oauth: true
|
||||
}
|
||||
});
|
||||
console.log(`Token for ${email} persisted to Prisma database`);
|
||||
} else {
|
||||
console.warn(`Account ${email} not found in Prisma, cannot persist tokens`);
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error(`Error persisting tokens to database for ${email}:`, dbError);
|
||||
// Don't fail the refresh if DB update fails - Redis cache is still updated
|
||||
}
|
||||
|
||||
return { accessToken: tokens.access_token, success: true };
|
||||
} catch (error) {
|
||||
console.error(`Error refreshing token for ${email}:`, error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user