Token Management Guide
Complete guide to managing OAuth tokens and API tokens in Taskee, including automatic token refresh and re-authentication flows
OAuth Token Management
Overview
Taskee implements automatic OAuth token refresh to ensure seamless integration with external services like Jira. This system handles token expiration, automatic refresh, and prompts users to re-authenticate when refresh tokens become invalid.
Architecture
┌────────────────┐
│ User Action │ (e.g., Import Tasks, Sync to Jira)
└────────┬───────┘
│
▼
┌────────────────────┐
│ API Route │
│ (Check Token) │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ TokenManager │
│ - Check expiry │
│ - Refresh if │
│ needed │
└────────┬───────────┘
│
├──────► Token Valid ────► Continue with Action
│
└──────► Token Invalid ─► Return 401 + Re-auth URL
Key Components
1. TokenManager (/lib/auth/token-manager.ts)
Central token management system with methods:
-
isTokenExpired(expiresAt, bufferMinutes)- Checks if token is expired or will expire soon
- Default buffer: 5 minutes
-
refreshJiraToken(refreshToken)- Attempts to refresh an expired token
- Returns success status and new tokens
- Detects invalid refresh tokens
-
ensureValidToken(projectId, adapterConfig)- Ensures a valid access token
- Automatically refreshes if needed
- Saves updated tokens to database
- Returns updated config or re-auth requirement
-
getReauthUrl(projectId, userId, adapterType)- Generates re-authentication URL
2. OAuth Status API (/app/api/auth/jira/check-oauth/route.ts)
Endpoint to check OAuth connection status:
GET /api/auth/jira/check-oauth?projectId={id}
Response (Valid Token):
{
"connected": true,
"tokenValid": true,
"expiresAt": "2025-11-13T10:00:00Z"
}
Response (Requires Re-auth):
{
"connected": false,
"tokenValid": false,
"requiresReauth": true,
"error": "Refresh token is invalid",
"reauthUrl": "/api/auth/jira/authorize?projectId=..."
}
3. OAuthStatusChecker Component (/components/auth/oauth-status-checker.tsx)
Client-side component that:
- Periodically checks OAuth status (every 5 minutes)
- Displays re-authentication prompt when needed
- Provides "Reconnect" button to start OAuth flow
4. Updated API Routes
All API routes that interact with external services now:
- Check token expiration BEFORE making requests
- Attempt automatic refresh if expired
- Return 401 with re-auth URL if refresh fails
Updated Routes:
/api/adapters/jira/tasks- Fetch tasks/api/tasks/[id]/sync- Sync task to Jira/api/tasks/import- Import tasks (if applicable)
How It Works
Scenario 1: Token Valid
// 1. API route receives request
const tokenResult = await TokenManager.ensureValidToken(projectId, config);
// 2. Token is still valid
// tokenResult = { config: originalConfig, requiresReauth: false }
// 3. Continue with original request
await adapter.connect(tokenResult.config);
Scenario 2: Token Expired, Refresh Succeeds
// 1. API route receives request
const tokenResult = await TokenManager.ensureValidToken(projectId, config);
// 2. Token expired, refresh attempted
// POST https://auth.atlassian.com/oauth/token
// { grant_type: "refresh_token", refresh_token: "..." }
// 3. New tokens received
// tokenResult = {
// config: { accessToken: "new...", expiresAt: "..." },
// requiresReauth: false
// }
// 4. Tokens saved to database
// 5. Continue with updated tokens
await adapter.connect(tokenResult.config);
Scenario 3: Refresh Token Invalid (Re-auth Required)
// 1. API route receives request
const tokenResult = await TokenManager.ensureValidToken(projectId, config);
// 2. Token expired, refresh attempted
// POST https://auth.atlassian.com/oauth/token
// Response: { error: "invalid_grant" }
// 3. Refresh failed
// tokenResult = {
// config: originalConfig,
// requiresReauth: true,
// error: "Refresh token is invalid..."
// }
// 4. Return 401 with re-auth URL
return NextResponse.json({
error: "Authentication expired",
requiresReauth: true,
reauthUrl: "/api/auth/jira/authorize?projectId=..."
}, { status: 401 });
// 5. Client shows re-authentication prompt
// 6. User clicks "Reconnect to Jira"
// 7. OAuth flow starts again
Error Handling
Common Error Codes
| Error | Cause | Solution |
|---|---|---|
invalid_grant | Refresh token expired/revoked | Re-authenticate |
unauthorized_client | Invalid client credentials | Check OAuth config |
invalid_client | Client ID/secret mismatch | Verify environment variables |
Handling in Code
const tokenResult = await TokenManager.ensureValidToken(projectId, config);
if (tokenResult.requiresReauth) {
// User needs to re-authenticate
return NextResponse.json({
error: "Authentication expired",
requiresReauth: true,
reauthUrl: tokenResult.reauthUrl
}, { status: 401 });
}
if (tokenResult.error) {
// Warning: refresh succeeded but there was an issue saving
console.warn("Token refresh warning:", tokenResult.error);
// Continue with request using updated tokens
}
// Use updated tokens
const config = tokenResult.config;
Integration Guide
Adding Token Check to New API Route
import { TokenManager } from "@/lib/auth/token-manager";
export async function POST(request: NextRequest) {
// ... fetch project and verify user ...
let adapterConfig = project.adapter_config as any;
// Check and refresh token if needed (OAuth only)
if (adapterConfig.authType === "oauth") {
const tokenResult = await TokenManager.ensureValidToken(
projectId,
adapterConfig
);
if (tokenResult.requiresReauth) {
return NextResponse.json({
error: "Authentication expired. Please reconnect.",
requiresReauth: true,
reauthUrl: TokenManager.getReauthUrl(projectId, user.id, "jira"),
}, { status: 401 });
}
// Use updated config
adapterConfig = tokenResult.config;
}
// Continue with your API logic...
const adapter = new JiraAdapter();
await adapter.connect(adapterConfig);
// ...
}
Adding OAuth Status Check to UI
import { OAuthStatusChecker } from "@/components/auth/oauth-status-checker";
export function MyComponent({ projectId, adapterConfig }) {
return (
<div>
{/* Show re-auth prompt if needed */}
{adapterConfig?.authType === "oauth" && (
<OAuthStatusChecker projectId={projectId} />
)}
{/* Your component content */}
</div>
);
}
Token Lifecycle
1. Initial OAuth Flow
↓
2. Store: access_token, refresh_token, expires_at
↓
3. Use access_token for API requests
↓
4. Check expiration before each request (5 min buffer)
↓
5a. Still Valid → Use existing token
5b. Expired → Refresh
↓
6a. Refresh Success → Update tokens, continue
6b. Refresh Fail → Prompt re-auth
↓
7. User re-authenticates (back to step 1)
Configuration
Environment Variables
JIRA_CLIENT_ID=your_jira_oauth_client_id
JIRA_CLIENT_SECRET=your_jira_oauth_client_secret
NEXT_PUBLIC_BASE_URL=https://your-domain.com
Token Expiry Settings
Modify buffer time in TokenManager:
// Default: 5 minutes
TokenManager.isTokenExpired(expiresAt, 5);
// Custom: 10 minutes
TokenManager.isTokenExpired(expiresAt, 10);
Check Interval
Modify OAuth status check interval in OAuthStatusChecker:
// Default: 5 minutes
const interval = setInterval(checkStatus, 5 * 60 * 1000);
// Custom: 10 minutes
const interval = setInterval(checkStatus, 10 * 60 * 1000);
Testing
Manual Testing
-
Test Token Refresh:
- Set token expiry to past date in database
- Make API request
- Verify token is refreshed automatically
-
Test Re-auth Flow:
- Set invalid refresh token in database
- Make API request
- Verify re-auth prompt appears
- Click "Reconnect" and complete OAuth
-
Test Status Checker:
- Load project page with OAuth connection
- Verify no warning if token valid
- Manually expire token
- Verify warning appears after next check
Database Testing
-- Check current token status
SELECT
id,
name,
adapter_config->>'authType' as auth_type,
adapter_config->>'expiresAt' as expires_at
FROM projects
WHERE adapter_type = 'jira';
-- Manually expire a token for testing
UPDATE projects
SET adapter_config = jsonb_set(
adapter_config,
'{expiresAt}',
'"2020-01-01T00:00:00Z"'
)
WHERE id = 'your-project-id';
Troubleshooting
Token Keeps Expiring
Cause: Short token lifetime from OAuth provider
Solution: Check OAuth provider settings, increase check frequency
Re-auth Loop
Cause: OAuth callback not saving tokens correctly
Solution: Verify callback route is saving access_token, refresh_token, and expiresAt
401 Errors on Every Request
Cause: Token refresh failing silently
Solution: Check logs for refresh errors, verify OAuth credentials
Database Not Updating
Cause: Permission issues or wrong project ID
Solution: Verify user owns project, check RLS policies
Security Considerations
-
Tokens in Database:
- Store encrypted at rest (Supabase handles this)
- Never expose in client-side code
- Use Row Level Security (RLS) policies
-
Token Refresh:
- Use HTTPS only
- Validate refresh token before use
- Log refresh attempts for audit
-
Re-authentication:
- Validate state parameter in OAuth flow
- Verify user ID matches
- Use secure redirect URLs
Future Enhancements
- Add token refresh queue to prevent race conditions
- Implement token caching for better performance
- Add support for multiple OAuth providers
- Monitor token refresh success/failure rates
- Add webhook support for token revocation events
- Implement automatic re-auth for background jobs