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

Commit 3bda0e4

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 3bda0e4

File tree

3 files changed

+115
-3
lines changed
  • packages

3 files changed

+115
-3
lines changed

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import time
34
from dataclasses import dataclass
45
from functools import wraps
56
from typing import ClassVar
@@ -23,7 +24,8 @@ def opt_oidc(f):
2324
@click.option("--username", help="OIDC username")
2425
@click.option("--password", help="OIDC password")
2526
@click.option("--connector-id", "connector_id", help="OIDC token exchange connector id (Dex specific)")
26-
@click.option("--callback-port",
27+
@click.option(
28+
"--callback-port",
2729
"callback_port",
2830
type=click.IntRange(0, 65535),
2931
default=None,
@@ -93,7 +95,7 @@ async def authorization_code_grant(self, callback_port: int | None = None):
9395
elif env_value.isdigit() and int(env_value) <= 65535:
9496
port = int(env_value)
9597
else:
96-
raise click.ClickException(f"Invalid {JMP_OIDC_CALLBACK_PORT} \"{env_value}\": must be a valid port")
98+
raise click.ClickException(f'Invalid {JMP_OIDC_CALLBACK_PORT} "{env_value}": must be a valid port')
9799

98100
tx, rx = create_memory_object_stream()
99101

@@ -133,8 +135,50 @@ async def callback(request):
133135

134136

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

138143

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