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:

  1. Check token expiration BEFORE making requests
  2. Attempt automatic refresh if expired
  3. 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

ErrorCauseSolution
invalid_grantRefresh token expired/revokedRe-authenticate
unauthorized_clientInvalid client credentialsCheck OAuth config
invalid_clientClient ID/secret mismatchVerify 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

  1. Test Token Refresh:

    • Set token expiry to past date in database
    • Make API request
    • Verify token is refreshed automatically
  2. Test Re-auth Flow:

    • Set invalid refresh token in database
    • Make API request
    • Verify re-auth prompt appears
    • Click "Reconnect" and complete OAuth
  3. 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

  1. Tokens in Database:

    • Store encrypted at rest (Supabase handles this)
    • Never expose in client-side code
    • Use Row Level Security (RLS) policies
  2. Token Refresh:

    • Use HTTPS only
    • Validate refresh token before use
    • Log refresh attempts for audit
  3. 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

References

Need help with tokens?

Check out the full documentation or get started with Taskee today.