Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit b0abca6

Browse files
committed
cli: add auth status
provide status of the current session: Token expiry: 2026-01-06 19:39:42 Status: Valid (9h 17m remaining) Subject: ... Issuer: https://... Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 8dd161a commit b0abca6

File tree

3 files changed

+118
-3
lines changed
  • packages

3 files changed

+118
-3
lines changed

packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def opt_oidc(f):
2323
@click.option("--username", help="OIDC username")
2424
@click.option("--password", help="OIDC password")
2525
@click.option("--connector-id", "connector_id", help="OIDC token exchange connector id (Dex specific)")
26-
@click.option("--callback-port",
26+
@click.option(
27+
"--callback-port",
2728
"callback_port",
2829
type=click.IntRange(0, 65535),
2930
default=None,
@@ -93,7 +94,7 @@ async def authorization_code_grant(self, callback_port: int | None = None):
9394
elif env_value.isdigit() and int(env_value) <= 65535:
9495
port = int(env_value)
9596
else:
96-
raise click.ClickException(f"Invalid {JMP_OIDC_CALLBACK_PORT} \"{env_value}\": must be a valid port")
97+
raise click.ClickException(f'Invalid {JMP_OIDC_CALLBACK_PORT} "{env_value}": must be a valid port')
9798

9899
tx, rx = create_memory_object_stream()
99100

@@ -133,8 +134,52 @@ async def callback(request):
133134

134135

135136
def decode_jwt(token: str):
136-
return json.loads(extract_compact(token.encode()).payload)
137+
try:
138+
return json.loads(extract_compact(token.encode()).payload)
139+
except (ValueError, KeyError, TypeError) as e:
140+
raise ValueError(f"Invalid JWT format: {e}") from e
137141

138142

139143
def decode_jwt_issuer(token: str):
140144
return decode_jwt(token).get("iss")
145+
146+
147+
def get_token_expiry(token: str) -> int | None:
148+
"""Get token expiry timestamp (Unix epoch seconds) from JWT.
149+
150+
Returns None if token doesn't have an exp claim.
151+
"""
152+
return decode_jwt(token).get("exp")
153+
154+
155+
def get_token_remaining_seconds(token: str) -> float | None:
156+
"""Get seconds remaining until token expires.
157+
158+
Returns:
159+
Positive value if token is still valid
160+
Negative value if token is expired (magnitude = how long ago)
161+
None if token doesn't have an exp claim
162+
"""
163+
import time
164+
165+
exp = get_token_expiry(token)
166+
if exp is None:
167+
return None
168+
return exp - time.time()
169+
170+
171+
def is_token_expired(token: str, buffer_seconds: int = 0) -> bool:
172+
"""Check if token is expired or will expire within buffer_seconds.
173+
174+
Args:
175+
token: JWT token string
176+
buffer_seconds: Consider expired if less than this many seconds remain
177+
178+
Returns:
179+
True if token is expired or will expire within buffer
180+
False if token is still valid (or has no exp claim)
181+
"""
182+
remaining = get_token_remaining_seconds(token)
183+
if remaining is None:
184+
return False
185+
return remaining < buffer_seconds
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from datetime import datetime, timezone
2+
3+
import click
4+
from jumpstarter_cli_common.config import opt_config
5+
from jumpstarter_cli_common.oidc import decode_jwt, get_token_remaining_seconds
6+
7+
8+
@click.group()
9+
def auth():
10+
"""
11+
Authentication and token management commands
12+
"""
13+
14+
15+
@auth.command(name="status")
16+
@opt_config(exporter=False)
17+
def token_status(config):
18+
"""
19+
Display token status and expiry information
20+
"""
21+
token_str = getattr(config, "token", None)
22+
23+
if not token_str:
24+
click.echo(click.style("No token found in config", fg="yellow"))
25+
return
26+
27+
try:
28+
payload = decode_jwt(token_str)
29+
except Exception as e:
30+
click.echo(click.style(f"Failed to decode token: {e}", fg="red"))
31+
return
32+
33+
exp = payload.get("exp")
34+
if not exp:
35+
click.echo(click.style("Token has no expiry claim", fg="yellow"))
36+
return
37+
38+
remaining = get_token_remaining_seconds(token_str)
39+
exp_dt = datetime.fromtimestamp(exp, tz=timezone.utc)
40+
41+
click.echo(f"Token expiry: {exp_dt.strftime('%Y-%m-%d %H:%M:%S')}")
42+
43+
if remaining is None:
44+
return
45+
46+
if remaining < 0:
47+
hours = int(abs(remaining) / 3600)
48+
mins = int((abs(remaining) % 3600) / 60)
49+
click.echo(click.style(f"Status: EXPIRED ({hours}h {mins}m ago)", fg="red", bold=True))
50+
click.echo(click.style("Run 'jmp login' to refresh your credentials.", fg="yellow"))
51+
elif remaining < 300: # Less than 5 minutes
52+
mins = int(remaining / 60)
53+
secs = int(remaining % 60)
54+
click.echo(click.style(f"Status: EXPIRING SOON ({mins}m {secs}s remaining)", fg="red", bold=True))
55+
click.echo(click.style("Run 'jmp login' to refresh your credentials.", fg="yellow"))
56+
elif remaining < 3600: # Less than 1 hour
57+
mins = int(remaining / 60)
58+
click.echo(click.style(f"Status: Valid ({mins}m remaining)", fg="yellow"))
59+
else:
60+
hours = int(remaining / 3600)
61+
mins = int((remaining % 3600) / 60)
62+
click.echo(click.style(f"Status: Valid ({hours}h {mins}m remaining)", fg="green"))
63+
64+
# Show additional token info
65+
if payload.get("sub"):
66+
click.echo(f"Subject: {payload.get('sub')}")
67+
if payload.get("iss"):
68+
click.echo(f"Issuer: {payload.get('iss')}")

packages/jumpstarter-cli/jumpstarter_cli/jmp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from jumpstarter_cli_common.version import version
66
from jumpstarter_cli_driver import driver
77

8+
from .auth import auth
89
from .config import config
910
from .create import create
1011
from .delete import delete
@@ -21,6 +22,7 @@ def jmp():
2122
"""The Jumpstarter CLI"""
2223

2324

25+
jmp.add_command(auth)
2426
jmp.add_command(create)
2527
jmp.add_command(delete)
2628
jmp.add_command(update)

0 commit comments

Comments
 (0)