11from datetime import datetime , timezone
22
33import click
4+ from jumpstarter_cli_common .blocking import blocking
45from jumpstarter_cli_common .config import opt_config
56from jumpstarter_cli_common .oidc import (
67 TOKEN_EXPIRY_WARNING_SECONDS ,
8+ Config ,
79 decode_jwt ,
10+ decode_jwt_issuer ,
811 format_duration ,
912 get_token_remaining_seconds ,
1013)
1114
15+ from jumpstarter .config .client import ClientConfigV1Alpha1
16+
1217
1318@click .group ()
1419def auth ():
@@ -19,21 +24,52 @@ def _print_token_status(remaining: float) -> None:
1924 """Print token status message based on remaining time."""
2025 duration = format_duration (remaining )
2126
27+ hint = "Run 'jmp login' to refresh your credentials."
28+
2229 if remaining < 0 :
2330 click .echo (click .style (f"Status: EXPIRED ({ duration } ago)" , fg = "red" , bold = True ))
24- click .echo (click .style ("Run 'jmp login --force' to refresh your credentials." , fg = "yellow" ))
31+ click .echo (click .style (hint , fg = "yellow" ))
2532 elif remaining < TOKEN_EXPIRY_WARNING_SECONDS :
2633 click .echo (click .style (f"Status: EXPIRING SOON ({ duration } remaining)" , fg = "red" , bold = True ))
27- click .echo (click .style ("Run 'jmp login --force' to refresh your credentials." , fg = "yellow" ))
34+ click .echo (click .style (hint , fg = "yellow" ))
2835 elif remaining < 3600 :
2936 click .echo (click .style (f"Status: Valid ({ duration } remaining)" , fg = "yellow" ))
3037 else :
3138 click .echo (click .style (f"Status: Valid ({ duration } remaining)" , fg = "green" ))
3239
3340
41+ def _print_subject_issuer (payload : dict ) -> None :
42+ sub = payload .get ("sub" )
43+ iss = payload .get ("iss" )
44+ if sub :
45+ click .echo (f"Subject: { sub } " )
46+ if iss :
47+ click .echo (f"Issuer: { iss } " )
48+
49+
50+ def _print_timestamp (label : str , value : int | None ) -> None :
51+ if value is None :
52+ return
53+ dt = datetime .fromtimestamp (value , tz = timezone .utc )
54+ click .echo (f"{ label } : { dt .strftime ('%Y-%m-%d %H:%M:%S %Z' )} " )
55+
56+
57+ def _print_verbose_details (payload : dict , config ) -> None :
58+ iat = payload .get ("iat" )
59+ auth_time = payload .get ("auth_time" )
60+ if isinstance (iat , int ):
61+ _print_timestamp ("Issued at" , iat )
62+ if isinstance (auth_time , int ):
63+ _print_timestamp ("Auth time" , auth_time )
64+
65+ refresh_token = getattr (config , "refresh_token" , None )
66+ click .echo (f"Refresh token stored: { 'yes' if refresh_token else 'no' } " )
67+
68+
3469@auth .command (name = "status" )
70+ @click .option ("--verbose" , is_flag = True , help = "Show additional token details" )
3571@opt_config (exporter = False )
36- def token_status (config ):
72+ def token_status (config , verbose : bool ):
3773 """Display token status and expiry information."""
3874 token_str = getattr (config , "token" , None )
3975
@@ -58,10 +94,38 @@ def token_status(config):
5894
5995 _print_token_status (remaining )
6096
61- # Show additional token info
62- sub = payload .get ("sub" )
63- iss = payload .get ("iss" )
64- if sub :
65- click .echo (f"Subject: { sub } " )
66- if iss :
67- click .echo (f"Issuer: { iss } " )
97+ _print_subject_issuer (payload )
98+
99+ if verbose :
100+ _print_verbose_details (payload , config )
101+
102+
103+ @auth .command (name = "refresh" )
104+ @opt_config (exporter = False )
105+ @blocking
106+ async def refresh_token (config ):
107+ """Refresh the access token using a stored refresh token."""
108+ refresh_token = getattr (config , "refresh_token" , None )
109+ if not refresh_token :
110+ raise click .ClickException ("No refresh token found. Run 'jmp login --offline-access'." )
111+
112+ access_token = getattr (config , "token" , None )
113+ if not access_token :
114+ raise click .ClickException ("No access token found. Run 'jmp login --offline-access'." )
115+
116+ try :
117+ issuer = decode_jwt_issuer (access_token )
118+ except Exception as e :
119+ raise click .ClickException (f"Failed to decode JWT issuer: { e } " ) from e
120+
121+ if issuer is None :
122+ raise click .ClickException ("Failed to determine issuer from access token." )
123+
124+ oidc = Config (issuer = issuer , client_id = "jumpstarter-cli" )
125+ tokens = await oidc .refresh_token_grant (refresh_token )
126+ config .token = tokens ["access_token" ]
127+ new_refresh_token = tokens .get ("refresh_token" )
128+ if new_refresh_token is not None :
129+ config .refresh_token = new_refresh_token
130+ ClientConfigV1Alpha1 .save (config ) # ty: ignore[invalid-argument-type]
131+ click .echo ("Access token refreshed." )
0 commit comments