# How Plane's Authentication System Works: Session Management, API Keys, and Token Refresh

> Discover how Plane's authentication works with session cookies, API keys, and magic links. Learn about its robust session management and unique token approach for secure access.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: deep-dive
- Published: 2026-06-22

---

**Plane uses three distinct authentication mechanisms: Django session cookies for web users, permanent API tokens with sliding `last_used` timestamps for programmatic access, and Redis-backed magic links for passwordless login, with no JWT refresh mechanism required for API keys.**

The open-source project management platform [Plane](https://github.com/makeplane/plane) employs a hybrid authentication architecture built on Django and Django REST Framework. The system splits authentication duties between browser-based sessions for the React frontend and stateless API keys for external integrations. This design eliminates the complexity of JWT refresh tokens while maintaining secure, trackable access for both humans and automated services.

## Session-Based Authentication for the Web UI

Plane's web interface relies on standard Django session authentication. When a user submits credentials to `POST /api/auth/login/`, Django's built-in authentication validates the email and password against the database.

Upon successful validation, the `SessionMiddleware` creates a signed session cookie (`sessionid`) and stores the session data in the `django_session` table. The cookie includes a timestamp controlled by `SESSION_COOKIE_AGE`, which defaults to two weeks. Each subsequent request automatically updates this timestamp, implementing a **sliding expiration** that keeps the session alive as long as the user remains active.

Logout occurs via `POST /api/auth/logout/`, which clears the client cookie and deletes the corresponding database entry. This mechanism is handled entirely by Django's contrib session framework, requiring no custom token management for browser-based workflows.

## API-Key Authentication for Programmatic Access

External clients and CI/CD pipelines authenticate using permanent API tokens transmitted via the `X-Api-Key` header. The validation logic resides in [`apps/api/plane/app/middleware/api_authentication.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/middleware/api_authentication.py) within the `APIKeyAuthentication` class:

```python
class APIKeyAuthentication(authentication.BaseAuthentication):
    auth_header_name = "X-Api-Key"

    def validate_api_token(self, token):
        api_token = APIToken.objects.get(
            Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
            token=token,
            is_active=True,
            user__is_active=True,
        )
        api_token.last_used = timezone.now()
        api_token.save(update_fields=["last_used"])
        return (api_token.user, api_token.token)

```

The `validate_api_token` method performs several critical checks:
- Verifies the token exists in the `APIToken` model (defined in [`apps/api/plane/models.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/models.py))
- Confirms the token is not expired based on the `expired_at` field
- Ensures both the token and the associated user remain active
- Updates the `last_used` timestamp to facilitate audit trails and stale token cleanup

Unlike JWT implementations, Plane does not issue refresh tokens. The `last_used` field acts as an activity tracker, allowing administrators to identify and revoke unused credentials without implementing complex token rotation logic.

### Token Creation and Rotation

Clients generate new API tokens by posting to `/user/api-tokens/` with the `APITokenCreateSerializer`. The endpoint returns the raw token value exactly once; thereafter, the system stores only a hash for security verification.

**Rotation workflow** involves creating a new token before the old one expires and deleting the compromised or outdated credential. This explicit rotation model eliminates the need for automatic refresh endpoints, as the client controls the lifecycle directly through the creation and deletion APIs implemented in [`apps/api/plane/app/views/user/api_tokens.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/user/api_tokens.py).

## Magic-Link and OAuth Authentication Flows

Plane supports passwordless authentication through email-based magic links and third-party OAuth providers. These flows temporarily store verification tokens in Redis rather than the database.

The magic link process works as follows:

1. **Generation**: `POST /api/auth/magic-generate/` creates a random token and stores it in Redis under the key `magic_<email>`, along with an expiration timestamp and attempt counter
2. **Delivery**: The system emails the user a URL containing the token
3. **Verification**: `POST /api/auth/magic-verify/` validates the token against the Redis entry, checks expiration, and immediately creates a standard Django session or issues a fresh API token
4. **Cleanup**: Upon successful verification, the Redis entry and attempt counter are deleted to prevent replay attacks

The OAuth flow follows a similar pattern after the provider redirects back to Plane, as implemented in [`apps/api/plane/authentication/adapter/oauth.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/authentication/adapter/oauth.py). Both flows ultimately converge on the same `User` model and permission system used by session and API-key authentication.

## Frontend Session Handling in React

The React frontend manages authentication through a thin service wrapper around the backend endpoints. The `AuthService` class in [`apps/web/core/services/auth.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/auth.service.ts) handles cookie-based sessions:

```typescript
export class AuthService {
  async login(email: string, password: string) {
    const resp = await fetch('/api/auth/login/', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
      credentials: 'include',   // sends/receives the session cookie
    });
    return resp.ok;
  }

  async logout() {
    await fetch('/api/auth/logout/', { method: 'POST', credentials: 'include' });
  }

  async createApiToken(data: ApiTokenPayload) {
    const resp = await fetch('/user/api-tokens/', {
      method: 'POST',
      body: JSON.stringify(data),
      credentials: 'include',
    });
    return resp.json();
  }
}

```

Setting `credentials: 'include'` ensures the browser transmits the `sessionid` cookie with each request, maintaining the authenticated state without manual token storage. When making API calls using an API key, the frontend retrieves the stored token from a secure location and injects it into the `X-Api-Key` header.

## Token Refresh and Session Lifecycle Management

Plane implements distinct lifecycle strategies for each authentication type:

- **Session Cookies**: Sliding expiration based on `SESSION_COOKIE_AGE`. Every request resets the timeout, keeping the session alive indefinitely during active use while allowing expiration after the configured idle period.

- **API Tokens**: No automatic refresh mechanism. Tokens remain valid until their explicit `expired_at` timestamp or until revoked. The `last_used` field provides metadata for housekeeping scripts but does not trigger automatic rotation.

- **Magic Links**: Single-use tokens with short Redis TTLs. Once consumed, they cannot be reused, and the resulting session follows standard cookie expiration rules.

This architecture simplifies the mental model for developers: browser users enjoy uninterrupted sessions, while API consumers manage token rotation explicitly through the creation endpoints.

## Summary

- **Session authentication** uses Django's signed cookies with sliding expiration, managed automatically by `SessionMiddleware` and updated on every request.
- **API keys** validate against the `APIToken` model in [`apps/api/plane/app/middleware/api_authentication.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/middleware/api_authentication.py), updating the `last_used` timestamp without requiring JWT refresh logic.
- **Magic links** store temporary tokens in Redis under `magic_<email>` keys, converting to permanent sessions upon verification via [`apps/api/plane/authentication/utils/user_auth_workflow.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/authentication/utils/user_auth_workflow.py).
- **Token rotation** occurs through explicit creation/deletion workflows at `/user/api-tokens/` rather than automated refresh grants.
- **React integration** relies on `credentials: 'include'` in [`apps/web/core/services/auth.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/auth.service.ts) to maintain cookie sessions across the frontend and backend.

## Frequently Asked Questions

### How does Plane handle API token expiration?

Plane checks the `expired_at` field in the `APIToken` model during every request validation. If the field is null, the token never expires. If populated, the `validate_api_token` method in [`apps/api/plane/app/middleware/api_authentication.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/middleware/api_authentication.py) verifies the current time against this timestamp. There is no automatic refresh mechanism; clients must create new tokens via the `/user/api-tokens/` endpoint before expiration.

### What is the difference between session and API key authentication in Plane?

Session authentication uses Django's built-in signed cookies (`sessionid`) stored in the browser and verified against the `django_session` table, ideal for the React web interface. API key authentication requires clients to send the `X-Api-Key` header on every request, with validation occurring in the `APIKeyAuthentication` middleware against the `APIToken` model. Sessions support sliding expiration, while API keys remain static until explicitly revoked or their `expired_at` timestamp passes.

### How does Plane's magic link authentication work?

Magic links generate a cryptographically random token stored temporarily in Redis under the key `magic_<email>`. The token has a short TTL and limited verification attempts. When the user clicks the link, `POST /api/auth/magic-verify/` checks the Redis entry, validates the token, and immediately creates a standard Django session or API token before deleting the Redis record to prevent reuse.

### Does Plane use JWT for authentication?

No, Plane does not implement JWT access tokens or refresh tokens. The system uses Django sessions for browser-based authentication and persistent database-stored API tokens for programmatic access. This design avoids the complexity of JWT key rotation and expiration handling while maintaining audit trails through the `last_used` field on API tokens.