makeplane/plane · error · AuthenticationException

5017

5017

Error message

BOT_USER_LOGIN_FORBIDDEN

What it means

Thrown at base.py:340 when the resolved user has is_bot=True. Bot identities (e.g. the WORKSPACE_SEED bot) are internal service accounts that act only through API tokens and must never be assumable via email/password, magic code, or OAuth. Because bots are provisioned internally and never created through the signup path, guarding only on an existing user record is sufficient.

Solutions

  1. Do not log in as a bot interactively; generate and use an API token for that bot identity instead.
  2. If a human must own that email, create a separate non-bot User account with a different email and migrate ownership.
  3. Confirm the target account is actually a bot (User.is_bot) before assuming misuse.

Example fix

# before: trying to sign in as bot@workspace via UI -> 5017
# after: use an API token for the bot
curl -H "X-API-Key: <bot-api-token>" https://plane.example.com/api/...
Defensive patterns

Strategy: validation

Validate before calling

from plane.db.models import User

def is_interactive_login_allowed(email: str) -> bool:
    u = User.objects.filter(email=email).first()
    return u is None or not u.is_bot  # None = new signup, allowed

Type guard

def is_bot_identity(user) -> bool:
    return user is not None and bool(user.is_bot)

Try / catch

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

Prevention

When it happens

Trigger: Any interactive login/signup where the email resolves to an existing User with is_bot=True raises AuthenticationException code 5017 with payload {email}. Hit when someone tries to sign in with the email address that a bot service account was registered under.

Common situations: An admin or script attempts to log in as the WORKSPACE_SEED bot or another bot user via the web UI or /auth/ endpoints. Also occurs if a bot's email was reused for a human account elsewhere and the human tries the wrong identity.

Understand the failure class

Related errors


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

Appendix: source

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

        # 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},
            )

        # True = new user (signup), False = returning user (login)
        is_signup = not bool(user)
        # If user is not present, create a new user
        if not user:
            # New user
            self.__check_signup(email)

            # Initialize user
            user = User(email=email, username=uuid.uuid4().hex)

            # Check if password is autoset
            if self.user_data.get("user").get("is_password_autoset"):
                user.set_password(uuid.uuid4().hex)

View on GitHub (pinned to 1c8a60f858)