test twenty

This commit is contained in:
alma 2026-01-01 19:31:48 +01:00
parent 144409502b
commit 2de985e78f
5 changed files with 370 additions and 15 deletions

3
.env
View File

@ -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
View 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
View 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)

View File

@ -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]

View File

@ -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);