Repository: crewAIInc/crewAI
Pull request #3089 (2025-07-01 00:29:09)

feat: add multiple provider support
# Multiple provider support (Auth0 to WorkOS migration)

This PR parametrizes the methods used for authenticating with CrewAI Enterprise platform and also the method to validate the JWT token received from the request.

- The JWT token validation is now done manually to remove the dependency from auth0 library.
- Removes the `crewai signup` command because it did the same thing as the `crewai login` command. Now just `crewai login` is used.
- Add WorkOS constants
- Adds a temporary step before starting the login process where users have to input their email. Then, a request is made to CrewAI Enterprise API to determine which provider the user is currently using, and then it redirects to the appropriate CLI authentication method.
  - For users that log in with google, or for new users: authenticate with WorkOS
  - For users that were created before the multiple provider support update that use email/password to log in: authenticate with Auth0
- Enhance printed messages (increase visibility of the authentication steps)
- Increases amount of polling attempts from 5 to 10
- Adds a `clear` method to the `Settings` model that erases all stored user settings
  - Add a call to `clear` every time an user performs `crewai login` (solves a bug that could happen when trying to reauthenticate but the user organization was deleted or changed)


# Important
This should be merged alongside
- https://github.com/crewAIInc/crewai-plus/pull/919

joaomdmoura: Disclaimer: This review was made by a crew of AI Agents.

# Code Review Comment for PR #3089

## Overview
This PR introduces significant changes to the authentication mechanisms within the CrewAI CLI by removing the separate signup command and updating the overall authentication flow. The transition to multiple authentication providers enhances flexibility, but it is crucial to ensure that the user experience remains straightforward.

## Code Quality Findings

### Security Improvements
- In `src/crewai/cli/authentication/main.py`, the token validation process could be fortified with additional checks. 

**Current Implementation:**
```python
def _validate_and_save_token(self, token_data: Dict[str, Any]) -> None:
    jwt_token_data = {
        "jwt_token": token_data["id_token"],
        ...
    }
```

**Suggested Improvement:**
```python
def _validate_and_save_token(self, token_data: Dict[str, Any]) -> None:
    if not token_data.get("id_token"):
        raise ValueError("Invalid token data: missing id_token")
        
    jwt_token_data = {
        "jwt_token": token_data["id_token"],
        ...
    }
    # Consider adding expiration validation
```
Integrating checks for required fields improves resilience against malformed input.

### Error Handling
- The error handling in `_poll_for_token()` could be enhanced to manage different HTTP responses effectively and to implement retry logic with exponential backoff.

**Improved Implementation:**
```python
def _poll_for_token(self, device_code_data: Dict[str, Any], client_id: str, token_poll_url: str) -> None:
    ...
    while attempts < max_attempts:
        try:
            ...
            if response.status_code == 429:  # Rate limiting
                time.sleep(2 ** attempts)  # Exponential backoff
            ...
        except requests.exceptions.RequestException as e:
            console.print(f"Network error: {str(e)}")
```
This approach ensures the system handles transient errors gracefully.

### Configuration Management
- The `clear()` method in `src/crewai/cli/config.py` could be more robust, ensuring settings are reset accurately and exceptions are properly managed.

**Suggested Implementation:**
```python
def clear(self) -> None:
    try:
        if self.config_path.exists():
            self.config_path.unlink()
        self.__init__(self.config_path)  # Reinitialize
    except Exception as e:
        raise ConfigurationError(f"Failed to clear settings: {str(e)}")
```
This not only enhances functionality but preserves user data integrity.

## Documentation Improvements
Documentation must reflect changes accurately:
- **Current Documentation**: 
    - Missing comprehensive user guidance about authentication flow.
  
- **Suggested Update**:
    ```markdown
    Deploy the crew or flow to [CrewAI Enterprise](https://app.crewai.com).
    - **Authentication**: Use `crewai login` to authenticate. This opens your default browser and establishes secure credentials.
    ```

## Lessons from Related PRs
- **Historical Context**: Reviewing previous authentication-related PRs emphasizes the trend toward a more unified command structure. The removal of separate commands aligns with enhancing user experience, a key area highlighted in past discussions.

## Recommendations for Improvement
1. **Type Hints**: Integrate type hints across the codebase for maintainability.
2. **Logging Enhancements**: Introduce structured logging within the authentication flows to aid in debugging.
3. **Testing**: Introduce comprehensive unit tests for the authentication workflows to cover both successful and error scenarios.

## Conclusion
The proposed changes significantly enhance CrewAI's authentication system, contributing to improved user experience and security. A focused user communication strategy and meticulous testing will be essential to facilitate these transitions. Overall, the direction of this PR is promising, and with the implementation of the suggested improvements, it will further solidify the robustness of the authentication mechanisms.

Patch:
diff --git a/docs/en/concepts/cli.mdx b/docs/en/concepts/cli.mdx
index 056b2660..804f9d76 100644
--- a/docs/en/concepts/cli.mdx
+++ b/docs/en/concepts/cli.mdx
@@ -4,6 +4,8 @@ description: Learn how to use the CrewAI CLI to interact with CrewAI.
 icon: terminal
 ---
 
+<Warning>Since release 0.140.0, CrewAI Enterprise started a process of migrating their login provider. As such, the authentication flow via CLI was updated. Users that use Google to login, or that created their account after July 3rd, 2025 will be unable to log in with older versions of the `crewai` library.</Warning>
+
 ## Overview
 
 The CrewAI CLI provides a set of commands to interact with CrewAI, allowing you to create, train, run, and manage crews & flows.
@@ -186,10 +188,7 @@ def crew(self) -> Crew:
 Deploy the crew or flow to [CrewAI Enterprise](https://app.crewai.com).
 
 - **Authentication**: You need to be authenticated to deploy to CrewAI Enterprise.
-    ```shell Terminal
-    crewai signup
-    ```
-    If you already have an account, you can login with:
+    You can login or create an account with:
     ```shell Terminal
     crewai login
     ```
diff --git a/docs/en/enterprise/guides/deploy-crew.mdx b/docs/en/enterprise/guides/deploy-crew.mdx
index e8c1a647..4f2f90cc 100644
--- a/docs/en/enterprise/guides/deploy-crew.mdx
+++ b/docs/en/enterprise/guides/deploy-crew.mdx
@@ -41,11 +41,8 @@ The CLI provides the fastest way to deploy locally developed crews to the Enterp
     First, you need to authenticate your CLI with the CrewAI Enterprise platform:
 
     ```bash
-    # If you already have a CrewAI Enterprise account
+    # If you already have a CrewAI Enterprise account, or want to create one:
     crewai login
-
-    # If you're creating a new account
-    crewai signup
     ```
 
     When you run either command, the CLI will:
diff --git a/docs/pt-BR/concepts/cli.mdx b/docs/pt-BR/concepts/cli.mdx
index bd255440..73ce4831 100644
--- a/docs/pt-BR/concepts/cli.mdx
+++ b/docs/pt-BR/concepts/cli.mdx
@@ -3,6 +3,7 @@ title: CLI
 description: Aprenda a usar o CLI do CrewAI para interagir com o CrewAI.
 icon: terminal
 ---
+<Warning>A partir da versão 0.140.0, a plataforma CrewAI Enterprise iniciou um processo de migração de seu provedor de login. Como resultado, o fluxo de autenticação via CLI foi atualizado. Usuários que utlizam o Google para fazer login, ou que criaram conta após 3 de julho de 2025 não poderão fazer login com versões anteriores da biblioteca `crewai`.</Warning>
 
 ## Visão Geral
 
diff --git a/docs/pt-BR/enterprise/guides/deploy-crew.mdx b/docs/pt-BR/enterprise/guides/deploy-crew.mdx
index 72c85441..4d22aa41 100644
--- a/docs/pt-BR/enterprise/guides/deploy-crew.mdx
+++ b/docs/pt-BR/enterprise/guides/deploy-crew.mdx
@@ -41,11 +41,8 @@ A CLI fornece a maneira mais rápida de implantar crews desenvolvidos localmente
     Primeiro, você precisa autenticar sua CLI com a plataforma CrewAI Enterprise:
 
     ```bash
-    # Se já possui uma conta CrewAI Enterprise
+    # Se já possui uma conta CrewAI Enterprise, ou deseja criar uma:
     crewai login
-
-    # Se vai criar uma nova conta
-    crewai signup
     ```
 
     Ao executar qualquer um dos comandos, a CLI irá:
diff --git a/pyproject.toml b/pyproject.toml
index affe38c3..df600b8a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,8 +27,8 @@ dependencies = [
     "openpyxl>=3.1.5",
     "pyvis>=0.3.2",
     # Authentication and Security
-    "auth0-python>=4.7.1",
     "python-dotenv>=1.0.0",
+    "pyjwt>=2.9.0",
     # Configuration and Utils
     "click>=8.1.7",
     "appdirs>=1.4.4",
diff --git a/src/crewai/cli/authentication/constants.py b/src/crewai/cli/authentication/constants.py
index 1bff9b36..c8c7dc5e 100644
--- a/src/crewai/cli/authentication/constants.py
+++ b/src/crewai/cli/authentication/constants.py
@@ -2,3 +2,7 @@ ALGORITHMS = ["RS256"]
 AUTH0_DOMAIN = "crewai.us.auth0.com"
 AUTH0_CLIENT_ID = "DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8"
 AUTH0_AUDIENCE = "https://crewai.us.auth0.com/api/v2/"
+
+WORKOS_DOMAIN = "login.crewai.com"
+WORKOS_CLI_CONNECT_APP_ID = "client_01JYT06R59SP0NXYGD994NFXXX"
+WORKOS_ENVIRONMENT_ID = "client_01JNJQWB4HG8T5980R5VHP057C"
diff --git a/src/crewai/cli/authentication/main.py b/src/crewai/cli/authentication/main.py
index 12308e3f..303c7a1f 100644
--- a/src/crewai/cli/authentication/main.py
+++ b/src/crewai/cli/authentication/main.py
@@ -5,37 +5,72 @@ from typing import Any, Dict
 import requests
 from rich.console import Console
 
-from .constants import AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN
-from .utils import TokenManager, validate_token
+from .constants import (
+    AUTH0_AUDIENCE,
+    AUTH0_CLIENT_ID,
+    AUTH0_DOMAIN,
+    WORKOS_DOMAIN,
+    WORKOS_CLI_CONNECT_APP_ID,
+    WORKOS_ENVIRONMENT_ID,
+)
+
+from .utils import TokenManager, validate_jwt_token
+from urllib.parse import quote
+from crewai.cli.plus_api import PlusAPI
+from crewai.cli.config import Settings
 
 console = Console()
 
 
 class AuthenticationCommand:
-    DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code"
-    TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
+    AUTH0_DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code"
+    AUTH0_TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
+
+    WORKOS_DEVICE_CODE_URL = f"https://{WORKOS_DOMAIN}/oauth2/device_authorization"
+    WORKOS_TOKEN_URL = f"https://{WORKOS_DOMAIN}/oauth2/token"
 
     def __init__(self):
         self.token_manager = TokenManager()
+        # TODO: WORKOS - This variable is temporary until migration to WorkOS is complete.
+        self.user_provider = "workos"
 
-    def signup(self) -> None:
+    def login(self) -> None:
         """Sign up to CrewAI+"""
-        console.print("Signing Up to CrewAI+ \n", style="bold blue")
-        device_code_data = self._get_device_code()
+
+        device_code_url = self.WORKOS_DEVICE_CODE_URL
+        token_url = self.WORKOS_TOKEN_URL
+        client_id = WORKOS_CLI_CONNECT_APP_ID
+        audience = None
+
+        console.print("Signing in to CrewAI Enterprise...\n", style="bold blue")
+
+        # TODO: WORKOS - Next line and conditional are temporary until migration to WorkOS is complete.
+        user_provider = self._determine_user_provider()
+        if user_provider == "auth0":
+            device_code_url = self.AUTH0_DEVICE_CODE_URL
+            token_url = self.AUTH0_TOKEN_URL
+            client_id = AUTH0_CLIENT_ID
+            audience = AUTH0_AUDIENCE
+            self.user_provider = "auth0"
+        # End of temporary code.
+
+        device_code_data = self._get_device_code(client_id, device_code_url, audience)
         self._display_auth_instructions(device_code_data)
 
-        return self._poll_for_token(device_code_data)
+        return self._poll_for_token(device_code_data, client_id, token_url)
 
-    def _get_device_code(self) -> Dict[str, Any]:
+    def _get_device_code(
+        self, client_id: str, device_code_url: str, audience: str | None = None
+    ) -> Dict[str, Any]:
         """Get the device code to authenticate the user."""
 
         device_code_payload = {
-            "client_id": AUTH0_CLIENT_ID,
+            "client_id": client_id,
             "scope": "openid",
-            "audience": AUTH0_AUDIENCE,
+            "audience": audience,
         }
         response = requests.post(
-            url=self.DEVICE_CODE_URL, data=device_code_payload, timeout=20
+            url=device_code_url, data=device_code_payload, timeout=20
         )
         response.raise_for_status()
         return response.json()
@@ -46,38 +81,33 @@ class AuthenticationCommand:
         console.print("2. Enter the following code: ", device_code_data["user_code"])
         webbrowser.open(device_code_data["verification_uri_complete"])
 
-    def _poll_for_token(self, device_code_data: Dict[str, Any]) -> None:
-        """Poll the server for the token."""
+    def _poll_for_token(
+        self, device_code_data: Dict[str, Any], client_id: str, token_poll_url: str
+    ) -> None:
+        """Polls the server for the token until it is received, or max attempts are reached."""
+
         token_payload = {
             "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
             "device_code": device_code_data["device_code"],
-            "client_id": AUTH0_CLIENT_ID,
+            "client_id": client_id,
         }
 
+        console.print("\nWaiting for authentication... ", style="bold blue", end="")
+
         attempts = 0
-        while True and attempts < 5:
-            response = requests.post(self.TOKEN_URL, data=token_payload, timeout=30)
+        while True and attempts < 10:
+            response = requests.post(token_poll_url, data=token_payload, timeout=30)
             token_data = response.json()
 
             if response.status_code == 200:
-                validate_token(token_data["id_token"])
-                expires_in = 360000  # Token expiration time in seconds
-                self.token_manager.save_tokens(token_data["access_token"], expires_in)
-
-                try:
-                    from crewai.cli.tools.main import ToolCommand
-                    ToolCommand().login()
-                except Exception:
-                    console.print(
-                        "\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
-                        style="yellow",
-                    )
-                    console.print(
-                        "Other features will work normally, but you may experience limitations "
-                        "with downloading and publishing tools."
-                        "\nRun [bold]crewai login[/bold] to try logging in again.\n",
-                        style="yellow",
-                    )
+                self._validate_and_save_token(token_data)
+
+                console.print(
+                    "Success!",
+                    style="bold green",
+                )
+
+                self._login_to_tool_repository()
 
                 console.print(
                     "\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n"
@@ -93,3 +123,88 @@ class AuthenticationCommand:
         console.print(
             "Timeout: Failed to get the token. Please try again.", style="bold red"
         )
+
+    def _validate_and_save_token(self, token_data: Dict[str, Any]) -> None:
+        """Validates the JWT token and saves the token to the token manager."""
+
+        jwt_token = token_data["access_token"]
+        jwt_token_data = {
+            "jwt_token": jwt_token,
+            "jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
+            "issuer": f"https://{WORKOS_DOMAIN}",
+            "audience": WORKOS_ENVIRONMENT_ID,
+        }
+
+        # TODO: WORKOS - The following conditional is temporary until migration to WorkOS is complete.
+        if self.user_provider == "auth0":
+            jwt_token_data["jwks_url"] = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
+            jwt_token_data["issuer"] = f"https://{AUTH0_DOMAIN}/"
+            jwt_token_data["audience"] = AUTH0_AUDIENCE
+
+        decoded_token = validate_jwt_token(**jwt_token_data)
+
+        expires_at = decoded_token.get("exp", 0)
+        self.token_manager.save_tokens(jwt_token, expires_at)
+
+    def _login_to_tool_repository(self) -> None:
+        """Login to the tool repository."""
+
+        from crewai.cli.tools.main import ToolCommand
+
+        try:
+            console.print(
+                "Now logging you in to the Tool Repository... ",
+                style="bold blue",
+                end="",
+            )
+
+            ToolCommand().login()
+
+            console.print(
+                "Success!\n",
+                style="bold green",
+            )
+
+            settings = Settings()
+            console.print(
+                f"You are authenticated to the tool repository as [bold cyan]'{settings.org_name}'[/bold cyan] ({settings.org_uuid})",
+                style="green",
+            )
+        except Exception:
+            console.print(
+                "\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
+                style="yellow",
+            )
+            console.print(
+                "Other features will work normally, but you may experience limitations "
+                "with downloading and publishing tools."
+                "\nRun [bold]crewai login[/bold] to try logging in again.\n",
+                style="yellow",
+            )
+
+    # TODO: WORKOS - This method is temporary until migration to WorkOS is complete.
+    def _determine_user_provider(self) -> str:
+        """Determine which provider to use for authentication."""
+
+        console.print(
+            "Enter your CrewAI Enterprise account email: ", style="bold blue", end=""
+        )
+        email = input()
+        email_encoded = quote(email)
+
+        # It's not correct to call this method directly, but it's temporary until migration is complete.
+        response = PlusAPI("")._make_request(
+            "GET", f"/crewai_plus/api/v1/me/provider?email={email_encoded}"
+        )
+
+        if response.status_code == 200:
+            if response.json().get("provider") == "auth0":
+                return "auth0"
+            else:
+                return "workos"
+        else:
+            console.print(
+                "Error: Failed to authenticate with crewai enterprise. Ensure that you are using the latest crewai version and please try again. If the problem persists, contact support@crewai.com.",
+                style="red",
+            )
+            raise SystemExit
diff --git a/src/crewai/cli/authentication/utils.py b/src/crewai/cli/authentication/utils.py
index 2f5fc183..e9ec6d45 100644
--- a/src/crewai/cli/authentication/utils.py
+++ b/src/crewai/cli/authentication/utils.py
@@ -1,32 +1,63 @@
 import json
 import os
 import sys
-from datetime import datetime, timedelta
+from datetime import datetime
 from pathlib import Path
 from typing import Optional
-
-from auth0.authentication.token_verifier import (
-    AsymmetricSignatureVerifier,
-    TokenVerifier,
-)
+import jwt
+from jwt import PyJWKClient
 from cryptography.fernet import Fernet
 
-from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN
-
 
-def validate_token(id_token: str) -> None:
+def validate_jwt_token(
+    jwt_token: str, jwks_url: str, issuer: str, audience: str
+) -> dict:
     """
-    Verify the token and its precedence
-
-    :param id_token:
+    Verify the token's signature and claims using PyJWT.
+    :param jwt_token: The JWT (JWS) string to validate.
+    :param jwks_url: The URL of the JWKS endpoint.
+    :param issuer: The expected issuer of the token.
+    :param audience: The expected audience of the token.
+    :return: The decoded token.
+    :raises Exception: If the token is invalid for any reason (e.g., signature mismatch,
+                       expired, incorrect issuer/audience, JWKS fetching error,
+                       missing required claims).
     """
-    jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
-    issuer = f"https://{AUTH0_DOMAIN}/"
-    signature_verifier = AsymmetricSignatureVerifier(jwks_url)
-    token_verifier = TokenVerifier(
-        signature_verifier=signature_verifier, issuer=issuer, audience=AUTH0_CLIENT_ID
-    )
-    token_verifier.verify(id_token)
+
+    decoded_token = None
+
+    try:
+        jwk_client = PyJWKClient(jwks_url)
+        signing_key = jwk_client.get_signing_key_from_jwt(jwt_token)
+
+        decoded_token = jwt.decode(
+            jwt_token,
+            signing_key.key,
+            algorithms=["RS256"],
+            audience=audience,
+            issuer=issuer,
+            options={
+                "verify_signature": True,
+                "verify_exp": True,
+                "verify_nbf": True,
+                "verify_iat": True,
+                "require": ["exp", "iat", "iss", "aud", "sub"],
+            },
+        )
+        return decoded_token
+
+    except jwt.ExpiredSignatureError:
+        raise Exception("Token has expired.")
+    except jwt.InvalidAudienceError:
+        raise Exception(f"Invalid token audience. Expected: '{audience}'")
+    except jwt.InvalidIssuerError:
+        raise Exception(f"Invalid token issuer. Expected: '{issuer}'")
+    except jwt.MissingRequiredClaimError as e:
+        raise Exception(f"Token is missing required claims: {str(e)}")
+    except jwt.exceptions.PyJWKClientError as e:
+        raise Exception(f"JWKS or key processing error: {str(e)}")
+    except jwt.InvalidTokenError as e:
+        raise Exception(f"Invalid token: {str(e)}")
 
 
 class TokenManager:
@@ -56,14 +87,14 @@ class TokenManager:
         self.save_secure_file(key_filename, new_key)
         return new_key
 
-    def save_tokens(self, access_token: str, expires_in: int) -> None:
+    def save_tokens(self, access_token: str, expires_at: int) -> None:
         """
         Save the access token and its expiration time.
 
         :param access_token: The access token to save.
-        :param expires_in: The expiration time of the access token in seconds.
+        :param expires_at: The UNIX timestamp of the expiration time.
         """
-        expiration_time = datetime.now() + timedelta(seconds=expires_in)
+        expiration_time = datetime.fromtimestamp(expires_at)
         data = {
             "access_token": access_token,
             "expiration": expiration_time.isoformat(),
diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py
index ab53c8bb..fe0804d9 100644
--- a/src/crewai/cli/cli.py
+++ b/src/crewai/cli/cli.py
@@ -2,7 +2,7 @@ from importlib.metadata import version as get_version
 from typing import Optional
 
 import click
-
+from crewai.cli.config import Settings
 from crewai.cli.add_crew_to_flow import add_crew_to_flow
 from crewai.cli.create_crew import create_crew
 from crewai.cli.create_flow import create_flow
@@ -138,8 +138,12 @@ def log_tasks_outputs() -> None:
 @click.option("-s", "--short", is_flag=True, help="Reset SHORT TERM memory")
 @click.option("-e", "--entities", is_flag=True, help="Reset ENTITIES memory")
 @click.option("-kn", "--knowledge", is_flag=True, help="Reset KNOWLEDGE storage")
-@click.option("-akn", "--agent-knowledge", is_flag=True, help="Reset AGENT KNOWLEDGE storage")
-@click.option("-k","--kickoff-outputs",is_flag=True,help="Reset LATEST KICKOFF TASK OUTPUTS")
+@click.option(
+    "-akn", "--agent-knowledge", is_flag=True, help="Reset AGENT KNOWLEDGE storage"
+)
+@click.option(
+    "-k", "--kickoff-outputs", is_flag=True, help="Reset LATEST KICKOFF TASK OUTPUTS"
+)
 @click.option("-a", "--all", is_flag=True, help="Reset ALL memories")
 def reset_memories(
     long: bool,
@@ -154,13 +158,23 @@ def reset_memories(
     Reset the crew memories (long, short, entity, latest_crew_kickoff_ouputs, knowledge, agent_knowledge). This will delete all the data saved.
     """
     try:
-        memory_types = [long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all]
+        memory_types = [
+            long,
+            short,
+            entities,
+            knowledge,
+            agent_knowledge,
+            kickoff_outputs,
+            all,
+        ]
         if not any(memory_types):
             click.echo(
                 "Please specify at least one memory type to reset using the appropriate flags."
             )
             return
-        reset_memories_command(long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all)
+        reset_memories_command(
+            long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all
+        )
     except Exception as e:
         click.echo(f"An error occurred while resetting memories: {e}", err=True)
 
@@ -210,16 +224,11 @@ def update():
     update_crew()
 
 
-@crewai.command()
-def signup():
-    """Sign Up/Login to CrewAI+."""
-    AuthenticationCommand().signup()
-
-
 @crewai.command()
 def login():
-    """Sign Up/Login to CrewAI+."""
-    AuthenticationCommand().signup()
+    """Sign Up/Login to CrewAI Enterprise."""
+    Settings().clear()
+    AuthenticationCommand().login()
 
 
 # DEPLOY CREWAI+ COMMANDS
diff --git a/src/crewai/cli/config.py b/src/crewai/cli/config.py
index 5d363f16..14b85f8d 100644
--- a/src/crewai/cli/config.py
+++ b/src/crewai/cli/config.py
@@ -37,6 +37,10 @@ class Settings(BaseModel):
         merged_data = {**file_data, **data}
         super().__init__(config_path=config_path, **merged_data)
 
+    def clear(self) -> None:
+        """Clear all settings"""
+        self.config_path.unlink(missing_ok=True)
+
     def dump(self) -> None:
         """Save current settings to settings.json"""
         if self.config_path.is_file():
diff --git a/src/crewai/cli/tools/main.py b/src/crewai/cli/tools/main.py
index ecdf972d..25cf89ee 100644
--- a/src/crewai/cli/tools/main.py
+++ b/src/crewai/cli/tools/main.py
@@ -156,7 +156,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
 
         console.print(f"Successfully installed {handle}", style="bold green")
 
-    def login(self):
+    def login(self) -> None:
         login_response = self.plus_api_client.login_to_tool_repository()
 
         if login_response.status_code != 200:
@@ -175,18 +175,10 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
         settings.tool_repository_password = login_response_json["credential"][
             "password"
         ]
-        settings.org_uuid = login_response_json["current_organization"][
-            "uuid"
-        ]
-        settings.org_name = login_response_json["current_organization"][
-            "name"
-        ]
+        settings.org_uuid = login_response_json["current_organization"]["uuid"]
+        settings.org_name = login_response_json["current_organization"]["name"]
         settings.dump()
 
-        console.print(
-            f"Successfully authenticated to the tool repository as {settings.org_name} ({settings.org_uuid}).", style="bold green"
-        )
-
     def _add_package(self, tool_details: dict[str, Any]):
         is_from_pypi = tool_details.get("source", None) == "pypi"
         tool_handle = tool_details["handle"]
@@ -243,9 +235,15 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
 
         return env
 
-    def _print_current_organization(self):
+    def _print_current_organization(self) -> None:
         settings = Settings()
         if settings.org_uuid:
-            console.print(f"Current organization: {settings.org_name} ({settings.org_uuid})", style="bold blue")
+            console.print(
+                f"Current organization: {settings.org_name} ({settings.org_uuid})",
+                style="bold blue",
+            )
         else:
-            console.print("No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.", style="yellow")
+            console.print(
+                "No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
+                style="yellow",
+            )
diff --git a/uv.lock b/uv.lock
index bbd82855..4cae3183 100644
--- a/uv.lock
+++ b/uv.lock
@@ -250,22 +250,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" },
 ]
 
-[[package]]
-name = "auth0-python"
-version = "4.7.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "aiohttp" },
-    { name = "cryptography" },
-    { name = "pyjwt" },
-    { name = "requests" },
-    { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/12/81/3e867262f1f48fdacb1f8e9853497f6283274ba2c3c145e767bc0c7ed3c8/auth0_python-4.7.2.tar.gz", hash = "sha256:5d36b7f26defa946c0a548dddccf0451fc62e9f8e61fd0138c5025ad2506ba8b", size = 73261, upload-time = "2024-09-11T06:23:38.03Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e4/0e/38cb7b781371e79e9c697fb78f3ccd18fda8bd547d0a2e76e616561a3792/auth0_python-4.7.2-py3-none-any.whl", hash = "sha256:df2224f9b1e170b3aa12d8bc7ff02eadb7cc229307a09ec6b8a55fd1e0e05dc8", size = 131834, upload-time = "2024-09-11T06:23:36.619Z" },
-]
-
 [[package]]
 name = "autoflake"
 version = "2.3.1"
@@ -661,7 +645,6 @@ name = "crewai"
 source = { editable = "." }
 dependencies = [
     { name = "appdirs" },
-    { name = "auth0-python" },
     { name = "blinker" },
     { name = "chromadb" },
     { name = "click" },
@@ -678,6 +661,7 @@ dependencies = [
     { name = "opentelemetry-sdk" },
     { name = "pdfplumber" },
     { name = "pydantic" },
+    { name = "pyjwt" },
     { name = "python-dotenv" },
     { name = "pyvis" },
     { name = "regex" },
@@ -737,7 +721,6 @@ requires-dist = [
     { name = "agentops", marker = "extra == 'agentops'", specifier = ">=0.3.0" },
     { name = "aisuite", marker = "extra == 'aisuite'", specifier = ">=0.1.10" },
     { name = "appdirs", specifier = ">=1.4.4" },
-    { name = "auth0-python", specifier = ">=4.7.1" },
     { name = "blinker", specifier = ">=1.9.0" },
     { name = "chromadb", specifier = ">=0.5.23" },
     { name = "click", specifier = ">=8.1.7" },
@@ -760,6 +743,7 @@ requires-dist = [
     { name = "pdfplumber", specifier = ">=0.11.4" },
     { name = "pdfplumber", marker = "extra == 'pdfplumber'", specifier = ">=0.11.4" },
     { name = "pydantic", specifier = ">=2.4.2" },
+    { name = "pyjwt", specifier = ">=2.9.0" },
     { name = "python-dotenv", specifier = ">=1.0.0" },
     { name = "pyvis", specifier = ">=0.3.2" },
     { name = "regex", specifier = ">=2024.9.11" },


Test patch:
diff --git a/tests/cli/authentication/test_auth_main.py b/tests/cli/authentication/test_auth_main.py
index 5d11d242..61511b5a 100644
--- a/tests/cli/authentication/test_auth_main.py
+++ b/tests/cli/authentication/test_auth_main.py
@@ -1,103 +1,419 @@
-import unittest
-from unittest.mock import MagicMock, patch
-
+import pytest
+from datetime import datetime, timedelta
 import requests
-
+from unittest.mock import MagicMock, patch, call
 from crewai.cli.authentication.main import AuthenticationCommand
+from crewai.cli.authentication.constants import (
+    AUTH0_AUDIENCE,
+    AUTH0_CLIENT_ID,
+    AUTH0_DOMAIN,
+    WORKOS_DOMAIN,
+    WORKOS_CLI_CONNECT_APP_ID,
+    WORKOS_ENVIRONMENT_ID,
+)
 
 
-class TestAuthenticationCommand(unittest.TestCase):
-    def setUp(self):
+class TestAuthenticationCommand:
+    def setup_method(self):
         self.auth_command = AuthenticationCommand()
 
-    @patch("crewai.cli.authentication.main.requests.post")
-    def test_get_device_code(self, mock_post):
-        mock_response = MagicMock()
-        mock_response.json.return_value = {
-            "device_code": "123456",
-            "user_code": "ABCDEF",
-            "verification_uri_complete": "https://example.com",
-            "interval": 5,
+    @pytest.mark.parametrize(
+        "user_provider,expected_urls",
+        [
+            (
+                "auth0",
+                {
+                    "device_code_url": f"https://{AUTH0_DOMAIN}/oauth/device/code",
+                    "token_url": f"https://{AUTH0_DOMAIN}/oauth/token",
+                    "client_id": AUTH0_CLIENT_ID,
+                    "audience": AUTH0_AUDIENCE,
+                },
+            ),
+            (
+                "workos",
+                {
+                    "device_code_url": f"https://{WORKOS_DOMAIN}/oauth2/device_authorization",
+                    "token_url": f"https://{WORKOS_DOMAIN}/oauth2/token",
+                    "client_id": WORKOS_CLI_CONNECT_APP_ID,
+                },
+            ),
+        ],
+    )
+    @patch(
+        "crewai.cli.authentication.main.AuthenticationCommand._determine_user_provider"
+    )
+    @patch("crewai.cli.authentication.main.AuthenticationCommand._get_device_code")
+    @patch(
+        "crewai.cli.authentication.main.AuthenticationCommand._display_auth_instructions"
+    )
+    @patch("crewai.cli.authentication.main.AuthenticationCommand._poll_for_token")
+    @patch("crewai.cli.authentication.main.console.print")
+    def test_login(
+        self,
+        mock_console_print,
+        mock_poll,
+        mock_display,
+        mock_get_device,
+        mock_determine_provider,
+        user_provider,
+        expected_urls,
+    ):
+        mock_determine_provider.return_value = user_provider
+        mock_get_device.return_value = {
+            "device_code": "test_code",
+            "user_code": "123456",
         }
-        mock_post.return_value = mock_response
 
-        device_code_data = self.auth_command._get_device_code()
+        self.auth_command.login()
 
-        self.assertEqual(device_code_data["device_code"], "123456")
-        self.assertEqual(device_code_data["user_code"], "ABCDEF")
-        self.assertEqual(
-            device_code_data["verification_uri_complete"], "https://example.com"
+        mock_console_print.assert_called_once_with(
+            "Signing in to CrewAI Enterprise...\n", style="bold blue"
+        )
+        mock_determine_provider.assert_called_once()
+        mock_get_device.assert_called_once_with(
+            expected_urls["client_id"],
+            expected_urls["device_code_url"],
+            expected_urls.get("audience", None),
+        )
+        mock_display.assert_called_once_with(
+            {"device_code": "test_code", "user_code": "123456"}
+        )
+        mock_poll.assert_called_once_with(
+            {"device_code": "test_code", "user_code": "123456"},
+            expected_urls["client_id"],
+            expected_urls["token_url"],
         )
-        self.assertEqual(device_code_data["interval"], 5)
 
+    @patch("crewai.cli.authentication.main.webbrowser")
     @patch("crewai.cli.authentication.main.console.print")
-    @patch("crewai.cli.authentication.main.webbrowser.open")
-    def test_display_auth_instructions(self, mock_open, mock_print):
+    def test_display_auth_instructions(self, mock_console_print, mock_webbrowser):
         device_code_data = {
-            "verification_uri_complete": "https://example.com",
-            "user_code": "ABCDEF",
+            "verification_uri_complete": "https://example.com/auth",
+            "user_code": "123456",
         }
 
         self.auth_command._display_auth_instructions(device_code_data)
 
-        mock_print.assert_any_call("1. Navigate to: ", "https://example.com")
-        mock_print.assert_any_call("2. Enter the following code: ", "ABCDEF")
-        mock_open.assert_called_once_with("https://example.com")
+        expected_calls = [
+            call("1. Navigate to: ", "https://example.com/auth"),
+            call("2. Enter the following code: ", "123456"),
+        ]
+        mock_console_print.assert_has_calls(expected_calls)
+        mock_webbrowser.open.assert_called_once_with("https://example.com/auth")
+
+    @pytest.mark.parametrize(
+        "user_provider,jwt_config",
+        [
+            (
+                "auth0",
+                {
+                    "jwks_url": f"https://{AUTH0_DOMAIN}/.well-known/jwks.json",
+                    "issuer": f"https://{AUTH0_DOMAIN}/",
+                    "audience": AUTH0_AUDIENCE,
+                },
+            ),
+            (
+                "workos",
+                {
+                    "jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
+                    "issuer": f"https://{WORKOS_DOMAIN}",
+                    "audience": WORKOS_ENVIRONMENT_ID,
+                },
+            ),
+        ],
+    )
+    @pytest.mark.parametrize("has_expiration", [True, False])
+    @patch("crewai.cli.authentication.main.validate_jwt_token")
+    @patch("crewai.cli.authentication.main.TokenManager.save_tokens")
+    def test_validate_and_save_token(
+        self,
+        mock_save_tokens,
+        mock_validate_jwt,
+        user_provider,
+        jwt_config,
+        has_expiration,
+    ):
+        self.auth_command.user_provider = user_provider
+        token_data = {"access_token": "test_access_token", "id_token": "test_id_token"}
+
+        if has_expiration:
+            future_timestamp = int((datetime.now() + timedelta(days=100)).timestamp())
+            decoded_token = {"exp": future_timestamp}
+        else:
+            decoded_token = {}
+
+        mock_validate_jwt.return_value = decoded_token
+
+        self.auth_command._validate_and_save_token(token_data)
+
+        mock_validate_jwt.assert_called_once_with(
+            jwt_token="test_access_token",
+            jwks_url=jwt_config["jwks_url"],
+            issuer=jwt_config["issuer"],
+            audience=jwt_config["audience"],
+        )
+
+        if has_expiration:
+            mock_save_tokens.assert_called_once_with(
+                "test_access_token", future_timestamp
+            )
+        else:
+            mock_save_tokens.assert_called_once_with("test_access_token", 0)
+
+    @patch("crewai.cli.tools.main.ToolCommand")
+    @patch("crewai.cli.authentication.main.Settings")
+    @patch("crewai.cli.authentication.main.console.print")
+    def test_login_to_tool_repository_success(
+        self, mock_console_print, mock_settings, mock_tool_command
+    ):
+        mock_tool_instance = MagicMock()
+        mock_tool_command.return_value = mock_tool_instance
+
+        mock_settings_instance = MagicMock()
+        mock_settings_instance.org_name = "Test Org"
+        mock_settings_instance.org_uuid = "test-uuid-123"
+        mock_settings.return_value = mock_settings_instance
+
+        self.auth_command._login_to_tool_repository()
+
+        mock_tool_command.assert_called_once()
+        mock_tool_instance.login.assert_called_once()
+
+        expected_calls = [
+            call(
+                "Now logging you in to the Tool Repository... ",
+                style="bold blue",
+                end="",
+            ),
+            call("Success!\n", style="bold green"),
+            call(
+                "You are authenticated to the tool repository as [bold cyan]'Test Org'[/bold cyan] (test-uuid-123)",
+                style="green",
+            ),
+        ]
+        mock_console_print.assert_has_calls(expected_calls)
 
     @patch("crewai.cli.tools.main.ToolCommand")
-    @patch("crewai.cli.authentication.main.requests.post")
-    @patch("crewai.cli.authentication.main.validate_token")
     @patch("crewai.cli.authentication.main.console.print")
-    def test_poll_for_token_success(
-        self, mock_print, mock_validate_token, mock_post, mock_tool
+    def test_login_to_tool_repository_error(
+        self, mock_console_print, mock_tool_command
     ):
+        mock_tool_instance = MagicMock()
+        mock_tool_instance.login.side_effect = Exception("Tool repository error")
+        mock_tool_command.return_value = mock_tool_instance
+
+        self.auth_command._login_to_tool_repository()
+
+        mock_tool_command.assert_called_once()
+        mock_tool_instance.login.assert_called_once()
+
+        expected_calls = [
+            call(
+                "Now logging you in to the Tool Repository... ",
+                style="bold blue",
+                end="",
+            ),
+            call(
+                "\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
+                style="yellow",
+            ),
+            call(
+                "Other features will work normally, but you may experience limitations with downloading and publishing tools.\nRun [bold]crewai login[/bold] to try logging in again.\n",
+                style="yellow",
+            ),
+        ]
+        mock_console_print.assert_has_calls(expected_calls)
+
+    @pytest.mark.parametrize(
+        "api_response,expected_provider",
+        [
+            ({"provider": "auth0"}, "auth0"),
+            ({"provider": "workos"}, "workos"),
+            ({"provider": "none"}, "workos"),  # Default to workos for any other value
+            (
+                {},
+                "workos",
+            ),  # Default to workos if no provider key is sent in the response
+        ],
+    )
+    @patch("crewai.cli.authentication.main.PlusAPI")
+    @patch("crewai.cli.authentication.main.console.print")
+    @patch("builtins.input", return_value="test@example.com")
+    def test_determine_user_provider_success(
+        self,
+        mock_input,
+        mock_console_print,
+        mock_plus_api,
+        api_response,
+        expected_provider,
+    ):
+        mock_api_instance = MagicMock()
         mock_response = MagicMock()
         mock_response.status_code = 200
-        mock_response.json.return_value = {
-            "id_token": "TOKEN",
-            "access_token": "ACCESS_TOKEN",
-        }
-        mock_post.return_value = mock_response
+        mock_response.json.return_value = api_response
+        mock_api_instance._make_request.return_value = mock_response
+        mock_plus_api.return_value = mock_api_instance
 
-        mock_instance = mock_tool.return_value
-        mock_instance.login.return_value = None
+        result = self.auth_command._determine_user_provider()
 
-        self.auth_command._poll_for_token({"device_code": "123456"})
+        mock_input.assert_called_once()
 
-        mock_validate_token.assert_called_once_with("TOKEN")
-        mock_print.assert_called_once_with(
-            "\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n"
+        mock_plus_api.assert_called_once_with("")
+        mock_api_instance._make_request.assert_called_once_with(
+            "GET", "/crewai_plus/api/v1/me/provider?email=test%40example.com"
         )
 
-    @patch("crewai.cli.authentication.main.requests.post")
+        assert result == expected_provider
+
+    @patch("crewai.cli.authentication.main.PlusAPI")
     @patch("crewai.cli.authentication.main.console.print")
-    def test_poll_for_token_error(self, mock_print, mock_post):
+    @patch("builtins.input", return_value="test@example.com")
+    def test_determine_user_provider_error(
+        self, mock_input, mock_console_print, mock_plus_api
+    ):
+        mock_api_instance = MagicMock()
+        mock_response = MagicMock()
+        mock_response.status_code = 500
+        mock_api_instance._make_request.return_value = mock_response
+        mock_plus_api.return_value = mock_api_instance
+
+        with pytest.raises(SystemExit):
+            self.auth_command._determine_user_provider()
+
+        mock_input.assert_called_once()
+
+        mock_plus_api.assert_called_once_with("")
+        mock_api_instance._make_request.assert_called_once_with(
+            "GET", "/crewai_plus/api/v1/me/provider?email=test%40example.com"
+        )
+
+        mock_console_print.assert_has_calls(
+            [
+                call(
+                    "Enter your CrewAI Enterprise account email: ",
+                    style="bold blue",
+                    end="",
+                ),
+                call(
+                    "Error: Failed to authenticate with crewai enterprise. Ensure that you are using the latest crewai version and please try again. If the problem persists, contact support@crewai.com.",
+                    style="red",
+                ),
+            ]
+        )
+
+    @patch("requests.post")
+    def test_get_device_code(self, mock_post):
         mock_response = MagicMock()
-        mock_response.status_code = 400
         mock_response.json.return_value = {
-            "error": "invalid_request",
-            "error_description": "Invalid request",
+            "device_code": "test_device_code",
+            "user_code": "123456",
+            "verification_uri_complete": "https://example.com/auth",
         }
         mock_post.return_value = mock_response
 
-        with self.assertRaises(requests.HTTPError):
-            self.auth_command._poll_for_token({"device_code": "123456"})
+        result = self.auth_command._get_device_code(
+            client_id="test_client",
+            device_code_url="https://example.com/device",
+            audience="test_audience",
+        )
+
+        mock_post.assert_called_once_with(
+            url="https://example.com/device",
+            data={
+                "client_id": "test_client",
+                "scope": "openid",
+                "audience": "test_audience",
+            },
+            timeout=20,
+        )
 
-        mock_print.assert_not_called()
+        assert result == {
+            "device_code": "test_device_code",
+            "user_code": "123456",
+            "verification_uri_complete": "https://example.com/auth",
+        }
 
-    @patch("crewai.cli.authentication.main.requests.post")
+    @patch("requests.post")
     @patch("crewai.cli.authentication.main.console.print")
-    def test_poll_for_token_timeout(self, mock_print, mock_post):
-        mock_response = MagicMock()
-        mock_response.status_code = 400
-        mock_response.json.return_value = {
-            "error": "authorization_pending",
-            "error_description": "Authorization pending",
+    def test_poll_for_token_success(self, mock_console_print, mock_post):
+        mock_response_success = MagicMock()
+        mock_response_success.status_code = 200
+        mock_response_success.json.return_value = {
+            "access_token": "test_access_token",
+            "id_token": "test_id_token",
         }
-        mock_post.return_value = mock_response
+        mock_post.return_value = mock_response_success
+
+        device_code_data = {"device_code": "test_device_code", "interval": 1}
+
+        with (
+            patch.object(
+                self.auth_command, "_validate_and_save_token"
+            ) as mock_validate,
+            patch.object(
+                self.auth_command, "_login_to_tool_repository"
+            ) as mock_tool_login,
+        ):
+            self.auth_command._poll_for_token(
+                device_code_data, "test_client", "https://example.com/token"
+            )
+
+            mock_post.assert_called_once_with(
+                "https://example.com/token",
+                data={
+                    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
+                    "device_code": "test_device_code",
+                    "client_id": "test_client",
+                },
+                timeout=30,
+            )
 
-        self.auth_command._poll_for_token({"device_code": "123456", "interval": 0.01})
+            mock_validate.assert_called_once()
+            mock_tool_login.assert_called_once()
 
-        mock_print.assert_called_once_with(
+            expected_calls = [
+                call("\nWaiting for authentication... ", style="bold blue", end=""),
+                call("Success!", style="bold green"),
+                call("\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n"),
+            ]
+            mock_console_print.assert_has_calls(expected_calls)
+
+    @patch("requests.post")
+    @patch("crewai.cli.authentication.main.console.print")
+    def test_poll_for_token_timeout(self, mock_console_print, mock_post):
+        mock_response_pending = MagicMock()
+        mock_response_pending.status_code = 400
+        mock_response_pending.json.return_value = {"error": "authorization_pending"}
+        mock_post.return_value = mock_response_pending
+
+        device_code_data = {
+            "device_code": "test_device_code",
+            "interval": 0.1,  # Short interval for testing
+        }
+
+        self.auth_command._poll_for_token(
+            device_code_data, "test_client", "https://example.com/token"
+        )
+
+        mock_console_print.assert_any_call(
             "Timeout: Failed to get the token. Please try again.", style="bold red"
         )
+
+    @patch("requests.post")
+    def test_poll_for_token_error(self, mock_post):
+        """Test the method to poll for token (error path)."""
+        # Setup mock to return error
+        mock_response_error = MagicMock()
+        mock_response_error.status_code = 400
+        mock_response_error.json.return_value = {
+            "error": "access_denied",
+            "error_description": "User denied access",
+        }
+        mock_post.return_value = mock_response_error
+
+        device_code_data = {"device_code": "test_device_code", "interval": 1}
+
+        with pytest.raises(requests.HTTPError):
+            self.auth_command._poll_for_token(
+                device_code_data, "test_client", "https://example.com/token"
+            )
diff --git a/tests/cli/authentication/test_utils.py b/tests/cli/authentication/test_utils.py
index 6da48dcd..0b2ed3cd 100644
--- a/tests/cli/authentication/test_utils.py
+++ b/tests/cli/authentication/test_utils.py
@@ -1,31 +1,110 @@
 import json
+import jwt
 import unittest
 from datetime import datetime, timedelta
 from unittest.mock import MagicMock, patch
 
 from cryptography.fernet import Fernet
 
-from crewai.cli.authentication.utils import TokenManager, validate_token
+from crewai.cli.authentication.utils import TokenManager, validate_jwt_token
 
 
+@patch("crewai.cli.authentication.utils.PyJWKClient", return_value=MagicMock())
+@patch("crewai.cli.authentication.utils.jwt")
 class TestValidateToken(unittest.TestCase):
-    @patch("crewai.cli.authentication.utils.AsymmetricSignatureVerifier")
-    @patch("crewai.cli.authentication.utils.TokenVerifier")
-    def test_validate_token(self, mock_token_verifier, mock_asymmetric_verifier):
-        mock_verifier_instance = mock_token_verifier.return_value
-        mock_id_token = "mock_id_token"
+    def test_validate_jwt_token(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.return_value = {"exp": 1719859200}
 
-        validate_token(mock_id_token)
+        # Create signing key object mock with a .key attribute
+        mock_pyjwkclient.return_value.get_signing_key_from_jwt.return_value = MagicMock(
+            key="mock_signing_key"
+        )
 
-        mock_asymmetric_verifier.assert_called_once_with(
-            "https://crewai.us.auth0.com/.well-known/jwks.json"
+        decoded_token = validate_jwt_token(
+            jwt_token="aaaaa.bbbbbb.cccccc",
+            jwks_url="https://mock_jwks_url",
+            issuer="https://mock_issuer",
+            audience="app_id_xxxx",
         )
-        mock_token_verifier.assert_called_once_with(
-            signature_verifier=mock_asymmetric_verifier.return_value,
-            issuer="https://crewai.us.auth0.com/",
-            audience="DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8",
+
+        mock_jwt.decode.assert_called_once_with(
+            "aaaaa.bbbbbb.cccccc",
+            "mock_signing_key",
+            algorithms=["RS256"],
+            audience="app_id_xxxx",
+            issuer="https://mock_issuer",
+            options={
+                "verify_signature": True,
+                "verify_exp": True,
+                "verify_nbf": True,
+                "verify_iat": True,
+                "require": ["exp", "iat", "iss", "aud", "sub"],
+            },
         )
-        mock_verifier_instance.verify.assert_called_once_with(mock_id_token)
+        mock_pyjwkclient.assert_called_once_with("https://mock_jwks_url")
+        self.assertEqual(decoded_token, {"exp": 1719859200})
+
+    def test_validate_jwt_token_expired(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.side_effect = jwt.ExpiredSignatureError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
+
+    def test_validate_jwt_token_invalid_audience(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.side_effect = jwt.InvalidAudienceError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
+
+    def test_validate_jwt_token_invalid_issuer(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.side_effect = jwt.InvalidIssuerError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
+
+    def test_validate_jwt_token_missing_required_claims(
+        self, mock_jwt, mock_pyjwkclient
+    ):
+        mock_jwt.decode.side_effect = jwt.MissingRequiredClaimError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
+
+    def test_validate_jwt_token_jwks_error(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.side_effect = jwt.exceptions.PyJWKClientError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
+
+    def test_validate_jwt_token_invalid_token(self, mock_jwt, mock_pyjwkclient):
+        mock_jwt.decode.side_effect = jwt.InvalidTokenError
+        with self.assertRaises(Exception):
+            validate_jwt_token(
+                jwt_token="aaaaa.bbbbbb.cccccc",
+                jwks_url="https://mock_jwks_url",
+                issuer="https://mock_issuer",
+                audience="app_id_xxxx",
+            )
 
 
 class TestTokenManager(unittest.TestCase):
@@ -62,9 +141,9 @@ class TestTokenManager(unittest.TestCase):
     @patch("crewai.cli.authentication.utils.TokenManager.save_secure_file")
     def test_save_tokens(self, mock_save):
         access_token = "test_token"
-        expires_in = 3600
+        expires_at = int((datetime.now() + timedelta(seconds=3600)).timestamp())
 
-        self.token_manager.save_tokens(access_token, expires_in)
+        self.token_manager.save_tokens(access_token, expires_at)
 
         mock_save.assert_called_once()
         args = mock_save.call_args[0]
@@ -73,11 +152,7 @@ class TestTokenManager(unittest.TestCase):
         data = json.loads(decrypted_data)
         self.assertEqual(data["access_token"], access_token)
         expiration = datetime.fromisoformat(data["expiration"])
-        self.assertAlmostEqual(
-            expiration,
-            datetime.now() + timedelta(seconds=expires_in),
-            delta=timedelta(seconds=1),
-        )
+        self.assertEqual(expiration, datetime.fromtimestamp(expires_at))
 
     @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
     def test_get_token_valid(self, mock_read):
diff --git a/tests/cli/cli_test.py b/tests/cli/cli_test.py
index 0ce74763..60e3208b 100644
--- a/tests/cli/cli_test.py
+++ b/tests/cli/cli_test.py
@@ -13,7 +13,7 @@ from crewai.cli.cli import (
     deply_status,
     flow_add_crew,
     reset_memories,
-    signup,
+    login,
     test,
     train,
     version,
@@ -261,12 +261,12 @@ def test_test_invalid_string_iterations(evaluate_crew, runner):
 
 
 @mock.patch("crewai.cli.cli.AuthenticationCommand")
-def test_signup(command, runner):
+def test_login(command, runner):
     mock_auth = command.return_value
-    result = runner.invoke(signup)
+    result = runner.invoke(login)
 
     assert result.exit_code == 0
-    mock_auth.signup.assert_called_once()
+    mock_auth.login.assert_called_once()
 
 
 @mock.patch("crewai.cli.cli.DeployCommand")
diff --git a/tests/cli/tools/test_main.py b/tests/cli/tools/test_main.py
index 182c3eb4..e61743ca 100644
--- a/tests/cli/tools/test_main.py
+++ b/tests/cli/tools/test_main.py
@@ -2,6 +2,7 @@ import os
 import tempfile
 import unittest
 import unittest.mock
+from datetime import datetime, timedelta
 from contextlib import contextmanager
 from unittest import mock
 from unittest.mock import MagicMock, patch
@@ -26,7 +27,9 @@ def in_temp_dir():
 
 @pytest.fixture
 def tool_command():
-    TokenManager().save_tokens("test-token", 36000)
+    TokenManager().save_tokens(
+        "test-token", (datetime.now() + timedelta(seconds=36000)).timestamp()
+    )
     tool_command = ToolCommand()
     with patch.object(tool_command, "login"):
         yield tool_command
@@ -57,7 +60,9 @@ def test_create_success(mock_subprocess, capsys, tool_command):
 @patch("crewai.cli.tools.main.subprocess.run")
 @patch("crewai.cli.plus_api.PlusAPI.get_tool")
 @patch("crewai.cli.tools.main.ToolCommand._print_current_organization")
-def test_install_success(mock_print_org, mock_get, mock_subprocess_run, capsys, tool_command):
+def test_install_success(
+    mock_print_org, mock_get, mock_subprocess_run, capsys, tool_command
+):
     mock_get_response = MagicMock()
     mock_get_response.status_code = 200
     mock_get_response.json.return_value = {
@@ -89,6 +94,7 @@ def test_install_success(mock_print_org, mock_get, mock_subprocess_run, capsys,
     # Verify _print_current_organization was called
     mock_print_org.assert_called_once()
 
+
 @patch("crewai.cli.tools.main.subprocess.run")
 @patch("crewai.cli.plus_api.PlusAPI.get_tool")
 def test_install_success_from_pypi(mock_get, mock_subprocess_run, capsys, tool_command):
@@ -169,7 +175,10 @@ def test_publish_when_not_in_sync(mock_is_synced, capsys, tool_command):
 )
 @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
 @patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False)
-@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}])
+@patch(
+    "crewai.cli.tools.main.extract_available_exports",
+    return_value=[{"name": "SampleTool"}],
+)
 @patch("crewai.cli.tools.main.ToolCommand._print_current_organization")
 def test_publish_when_not_in_sync_and_force(
     mock_print_org,
@@ -223,7 +232,10 @@ def test_publish_when_not_in_sync_and_force(
 )
 @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
 @patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=True)
-@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}])
+@patch(
+    "crewai.cli.tools.main.extract_available_exports",
+    return_value=[{"name": "SampleTool"}],
+)
 def test_publish_success(
     mock_available_exports,
     mock_is_synced,
@@ -273,7 +285,10 @@ def test_publish_success(
     read_data=b"sample tarball content",
 )
 @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
-@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}])
+@patch(
+    "crewai.cli.tools.main.extract_available_exports",
+    return_value=[{"name": "SampleTool"}],
+)
 def test_publish_failure(
     mock_available_exports,
     mock_publish,
@@ -311,7 +326,10 @@ def test_publish_failure(
     read_data=b"sample tarball content",
 )
 @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
-@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}])
+@patch(
+    "crewai.cli.tools.main.extract_available_exports",
+    return_value=[{"name": "SampleTool"}],
+)
 def test_publish_api_error(
     mock_available_exports,
     mock_publish,
@@ -338,7 +356,6 @@ def test_publish_api_error(
     mock_publish.assert_called_once()
 
 
-
 @patch("crewai.cli.tools.main.Settings")
 def test_print_current_organization_with_org(mock_settings, capsys, tool_command):
     mock_settings_instance = MagicMock()