How Plane's Authentication System Works: Session Management, API Keys, and Token Refresh
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 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 within the APIKeyAuthentication class:
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
APITokenmodel (defined inapps/api/plane/models.py) - Confirms the token is not expired based on the
expired_atfield - Ensures both the token and the associated user remain active
- Updates the
last_usedtimestamp 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.
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:
- Generation:
POST /api/auth/magic-generate/creates a random token and stores it in Redis under the keymagic_<email>, along with an expiration timestamp and attempt counter - Delivery: The system emails the user a URL containing the token
- 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 - 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. 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 handles cookie-based sessions:
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_attimestamp or until revoked. Thelast_usedfield 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
SessionMiddlewareand updated on every request. - API keys validate against the
APITokenmodel inapps/api/plane/app/middleware/api_authentication.py, updating thelast_usedtimestamp without requiring JWT refresh logic. - Magic links store temporary tokens in Redis under
magic_<email>keys, converting to permanent sessions upon verification viaapps/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'inapps/web/core/services/auth.service.tsto 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →