Skip to content

Commit 6100c25

Browse files
committed
kci: Add token-info command
Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
1 parent 8ba61e7 commit 6100c25

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

kernelci/cli/user.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
#
55
"""User management commands"""
66

7+
import base64
8+
import json
9+
import datetime
10+
import time
11+
12+
import click
13+
714
from . import Args, catch_error, echo_json, get_api, kci
815

916

@@ -22,3 +29,77 @@ def whoami(config, api, indent, secrets):
2229
api = get_api(config, api, secrets)
2330
user = api.user.whoami()
2431
echo_json(user, indent)
32+
33+
34+
def _b64url_decode(data: str) -> bytes:
35+
"""Decode base64url data with padding."""
36+
padding = '=' * (-len(data) % 4)
37+
return base64.urlsafe_b64decode(data + padding)
38+
39+
40+
def _decode_jwt(token: str) -> dict:
41+
"""Decode a JWT without verifying signature."""
42+
parts = token.split('.')
43+
if len(parts) != 3:
44+
raise click.ClickException("Invalid JWT format; expected 3 parts")
45+
try:
46+
header = json.loads(_b64url_decode(parts[0]).decode('utf-8'))
47+
payload = json.loads(_b64url_decode(parts[1]).decode('utf-8'))
48+
except (ValueError, json.JSONDecodeError) as exc:
49+
raise click.ClickException("Invalid JWT encoding") from exc
50+
return {"header": header, "payload": payload}
51+
52+
53+
@kci_user.command(secrets=True, name='token-info')
54+
@click.option('--token', help="JWT token to decode (defaults to API token)")
55+
@click.option('--raw', is_flag=True, help="Print raw JSON data")
56+
@Args.api
57+
@Args.indent
58+
@catch_error
59+
def token_info(token, raw, api, indent, secrets):
60+
"""Decode a JWT and report expiration status"""
61+
token = token or (secrets.api.token if secrets else None)
62+
if not token:
63+
raise click.ClickException(
64+
"No token provided and no API token found; use --token or set "
65+
f"kci.secrets.api.<name>.token and pass --api (current: {api})"
66+
)
67+
68+
data = _decode_jwt(token)
69+
if api:
70+
data["api"] = api
71+
payload = data.get("payload", {})
72+
exp = payload.get("exp")
73+
now = int(time.time())
74+
exp_info = None
75+
if isinstance(exp, (int, float)):
76+
exp_int = int(exp)
77+
exp_info = {
78+
"exp": exp_int,
79+
"now": now,
80+
"expired": exp_int <= now,
81+
"expires_in_seconds": max(0, exp_int - now),
82+
}
83+
data["expiration"] = exp_info
84+
85+
if raw:
86+
echo_json(data, indent)
87+
return
88+
89+
header = data.get("header") or {}
90+
click.echo("Token information:")
91+
if api:
92+
click.echo(f" API config: {api}")
93+
click.echo(f" Algorithm: {header.get('alg', 'unknown')}")
94+
click.echo(f" Type: {header.get('typ', 'unknown')}")
95+
96+
if exp_info is None:
97+
click.echo(" Expiration: not present")
98+
return
99+
100+
exp_dt = datetime.datetime.utcfromtimestamp(exp_info["exp"])
101+
now_dt = datetime.datetime.utcfromtimestamp(exp_info["now"])
102+
click.echo(f" Expires (UTC): {exp_dt.isoformat()}Z")
103+
click.echo(f" Now (UTC): {now_dt.isoformat()}Z")
104+
click.echo(f" Expired: {str(exp_info['expired']).lower()}")
105+
click.echo(f" Expires in (seconds): {exp_info['expires_in_seconds']}")

0 commit comments

Comments
 (0)