From 8ac725603a5b4f8582d62946fcc76ca630637641 Mon Sep 17 00:00:00 2001 From: Benny Zlotnik Date: Tue, 13 Jan 2026 18:49:28 +0200 Subject: [PATCH] cli: add jmp login --force In order to force a new login and get token with full lifetime if current one is about to expire Signed-off-by: Benny Zlotnik --- .../jumpstarter_cli_common/oidc.py | 9 +++++++-- packages/jumpstarter-cli/jumpstarter_cli/auth.py | 4 ++-- .../jumpstarter-cli/jumpstarter_cli/login.py | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py index f382caa4f..5f4115fbd 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py @@ -82,7 +82,7 @@ async def password_grant(self, username: str, password: str): ) ) - async def authorization_code_grant(self, callback_port: int | None = None): + async def authorization_code_grant(self, callback_port: int | None = None, prompt: str | None = None): config = await self.configuration() # Use provided port, fall back to env var, then default to 0 (OS picks) @@ -120,7 +120,12 @@ async def callback(request): client = self.client(redirect_uri=redirect_uri) - uri, state = client.create_authorization_url(config["authorization_endpoint"]) + # Add prompt parameter if force requested + auth_params = {} + if prompt: + auth_params["prompt"] = prompt + + uri, state = client.create_authorization_url(config["authorization_endpoint"], **auth_params) print("Please open the URL in browser: ", uri) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/auth.py b/packages/jumpstarter-cli/jumpstarter_cli/auth.py index d06142519..630b7d11f 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/auth.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/auth.py @@ -21,10 +21,10 @@ def _print_token_status(remaining: float) -> None: if remaining < 0: click.echo(click.style(f"Status: EXPIRED ({duration} ago)", fg="red", bold=True)) - click.echo(click.style("Run 'jmp login' to refresh your credentials.", fg="yellow")) + click.echo(click.style("Run 'jmp login --force' to refresh your credentials.", fg="yellow")) elif remaining < TOKEN_EXPIRY_WARNING_SECONDS: click.echo(click.style(f"Status: EXPIRING SOON ({duration} remaining)", fg="red", bold=True)) - click.echo(click.style("Run 'jmp login' to refresh your credentials.", fg="yellow")) + click.echo(click.style("Run 'jmp login --force' to refresh your credentials.", fg="yellow")) elif remaining < 3600: click.echo(click.style(f"Status: Valid ({duration} remaining)", fg="yellow")) else: diff --git a/packages/jumpstarter-cli/jumpstarter_cli/login.py b/packages/jumpstarter-cli/jumpstarter_cli/login.py index c03800239..60dcfc3ff 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/login.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/login.py @@ -19,6 +19,12 @@ @click.option("-e", "--endpoint", type=str, help="Enter the Jumpstarter service endpoint.", default=None) @click.option("--namespace", type=str, help="Enter the Jumpstarter exporter namespace.", default=None) @click.option("--name", type=str, help="Enter the Jumpstarter exporter name.", default=None) +@click.option( + "--force", + is_flag=True, + help="Force fresh login", + default=False, +) @opt_oidc # client specific # TODO: warn if used with exporter @@ -52,6 +58,7 @@ async def login( # noqa: C901 insecure_tls_config: bool, nointeractive: bool, allow, + force: bool, ): """Login into a jumpstarter instance""" @@ -124,7 +131,8 @@ async def login( # noqa: C901 elif username is not None and password is not None: tokens = await oidc.password_grant(username, password) else: - tokens = await oidc.authorization_code_grant(callback_port=callback_port) + prompt = "login" if force else None + tokens = await oidc.authorization_code_grant(callback_port=callback_port, prompt=prompt) config.token = tokens["access_token"] @@ -138,10 +146,11 @@ async def login( # noqa: C901 case "exporter_config": ExporterConfigV1Alpha1.save(config, value) # ty: ignore[invalid-argument-type] + @blocking async def relogin_client(config: ClientConfigV1Alpha1): """Relogin into a jumpstarter instance""" - client_id = "jumpstarter-cli" # TODO: store this metadata in the config + client_id = "jumpstarter-cli" # TODO: store this metadata in the config try: issuer = decode_jwt_issuer(config.token) except Exception as e: @@ -151,7 +160,6 @@ async def relogin_client(config: ClientConfigV1Alpha1): oidc = Config(issuer=issuer, client_id=client_id) tokens = await oidc.authorization_code_grant() config.token = tokens["access_token"] - ClientConfigV1Alpha1.save(config) # ty: ignore[invalid-argument-type] + ClientConfigV1Alpha1.save(config) # ty: ignore[invalid-argument-type] except Exception as e: raise ReauthenticationFailed(f"Failed to re-authenticate: {e}") from e -