makeplane/plane · error · AuthenticationException

5019

5019

Error message

USER_ACCOUNT_DEACTIVATED

What it means

Thrown at base.py:327 when an email matches a User row that is both inactive (is_active=False) and has a recorded last_logout_time. It is the explicit guard for GHSA-rmmf-rj2q-3rrg: an account deactivated via the deactivation API must never log back in through any interactive flow. Using last_logout_time (rather than last_login_time) as the discriminator lets a never-logged-in provisioned account still complete its first login.

Source

Thrown at apps/api/plane/authentication/adapter/base.py:327

    def complete_login_or_signup(self):
        # Get email
        email = self.user_data.get("email")

        # Sanitize email
        email = self.sanitize_email(email)

        # Check if the user is present
        user = User.objects.filter(email=email).first()

        # Reject explicitly-deactivated accounts (GHSA-rmmf-rj2q-3rrg).
        # The deactivation endpoint always sets last_logout_time, so using it
        # as the discriminator is more reliable than last_login_time: a
        # provisioned account that was never deactivated has last_logout_time=None
        # and is allowed through for its first login; an account deactivated via
        # the API has last_logout_time set and is blocked regardless of whether
        # it had previously logged in.
        if user and not user.is_active and user.last_logout_time is not None:
            raise AuthenticationException(
                error_code=AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
                error_message="USER_ACCOUNT_DEACTIVATED",
                payload={"email": email},
            )

        # Reject bot service accounts (BOT_USER_LOGIN_FORBIDDEN). Bots (is_bot=True,
        # e.g. the WORKSPACE_SEED bot) are internal identities that act only through
        # API tokens; they must never be assumable via the interactive login/signup
        # flow (email/password, magic code, or any OAuth provider). A brand-new
        # signup can never be a bot — bots are provisioned internally, never through
        # this path — so guarding on an existing `user` record is sufficient.
        if user and user.is_bot:
            raise AuthenticationException(
                error_code=AUTHENTICATION_ERROR_CODES["BOT_USER_LOGIN_FORBIDDEN"],
                error_message="BOT_USER_LOGIN_FORBIDDEN",
                payload={"email": email},
            )

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Reactivate the account: set is_active=True and clear last_logout_time=None on the User (admin or Django shell), then retry login.
  2. If reactivation is intentional and self-service, expose it through the password-reset/reactivation flow rather than manual DB edits.
  3. Verify the deactivation was not accidental by auditing last_logout_time and the admin action log for that user.

Example fix

// before: user cannot log in after deactivation
// after (reactivate in shell/manage.py):
// python manage.py shell
// >>> from plane.db.models import User
// >>> u = User.objects.get(email='jane@x.com')
// >>> u.is_active = True
// >>> u.last_logout_time = None
// >>> u.save()
Defensive patterns

Strategy: validation

Validate before calling

from plane.db.models import User

def can_attempt_login(email: str) -> bool:
    u = User.objects.filter(email=email).first()
    # False when deactivated with a recorded logout -> would raise 5019
    return u is None or u.is_active or u.last_logout_time is None

Type guard

def is_login_blocked_by_deactivation(user) -> bool:
    return user is not None and not user.is_active and user.last_logout_time is not None

Try / catch

from plane.authentication.adapter.error import AuthenticationException

try:
    adapter_login(email, password)
except AuthenticationException as e:
    if e.error_code == 5019:
        show_reactivation_prompt(email=e.payload.get('email'))
    else:
        raise

Prevention

When it happens

Trigger: The adapter (base.py:~310) looks up User.objects.filter(email=email).first() during any sign-in/sign-up. If user.is_active is False AND user.last_logout_time is not None, AuthenticationException is raised with code 5019 and payload {email}. Reached from email/password, magic-code, and every OAuth provider because they all funnel through this shared pre-flight.

Common situations: User was deactivated by an admin or via the account-deactivation endpoint, then attempts to log in again before being reactivated. Also seen in test/staging environments that copy production users without resetting is_active/last_logout_time.

Related errors


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/10af3841d5c9803d. Report an issue: GitHub.