Skip to content

Commit

Permalink
feat: extract the expiration from the token, show for refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
ctron committed Apr 23, 2024
1 parent a51568d commit c8d611c
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ num-traits = "0.2"
openid = "0.14"
reqwest = "0.12"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
simplelog = "0.12"
time = { version = "0.3", features = ["serde-well-known", "formatting"] }
Expand Down
42 changes: 42 additions & 0 deletions src/claims.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use openid::{CompactJson, SingleOrMultiple};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;

/// Access token claims
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccessTokenClaims {
#[serde(default)]
pub azp: Option<String>,
pub sub: String,
pub iss: Url,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aud: Option<SingleOrMultiple<String>>,

#[serde(default)]
pub exp: Option<i64>,
#[serde(default)]
pub iat: Option<i64>,
#[serde(default)]
pub auth_time: Option<i64>,

#[serde(flatten)]
pub extended_claims: Value,

#[serde(default, skip_serializing_if = "String::is_empty")]
pub scope: String,
}

impl CompactJson for AccessTokenClaims {}

/// Access token claims
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RefreshTokenClaims {
#[serde(default)]
pub exp: Option<i64>,

#[serde(flatten)]
pub extended_claims: Value,
}

impl CompactJson for RefreshTokenClaims {}
118 changes: 89 additions & 29 deletions src/cmd/list.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use crate::config::{ClientType, Config};
use crate::{
claims::{AccessTokenClaims, RefreshTokenClaims},
config::{ClientType, Config},
};
use comfy_table::{presets, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
use openid::{biscuit::jws::Compact, CompactJson, Empty};
use std::path::PathBuf;
use time::macros::format_description;
use time::OffsetDateTime;
use time::{macros::format_description, OffsetDateTime};

/// List configured clients
#[derive(Debug, clap::Parser)]
#[command(rename_all_env = "SNAKE_CASE")]
pub struct List {
#[arg(from_global)]
pub config: Option<PathBuf>,

/// Show more details
#[arg(short, long)]
pub details: bool,
}

impl List {
Expand All @@ -20,7 +27,14 @@ impl List {
table
.load_preset(presets::ASCII_MARKDOWN)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(["Name", "Issuer", "Client", "Public", "Access Token"]);
.set_header([
"Name",
"Issuer",
"Client",
"Public",
"Access Token",
"Refresh Token",
]);

for (name, client) in config.clients {
let mut row = Row::new();
Expand All @@ -39,31 +53,31 @@ impl List {
}

if let Some(state) = &client.state {
match state.expires {
None => {
row.add_cell("∞".into());
}
Some(expires) => {
let rem = expires - OffsetDateTime::now_utc();

// truncate to seconds
let format_rem = humantime::Duration::from(std::time::Duration::from_secs(
rem.unsigned_abs().as_secs(),
));
let expires = expires.format(format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second]Z"
))?;

let cell = if rem.is_positive() {
Cell::new(format!("valid: {format_rem} ({expires})")).fg(Color::Green)
} else {
Cell::new(format!("expired: {format_rem} ({expires})"))
.fg(Color::DarkGrey)
};

row.add_cell(cell);
}
}
let access = self.token::<_, AccessTokenClaims>(&state.access_token, |token| {
self.expiration(
token
.exp
.and_then(|exp| OffsetDateTime::from_unix_timestamp(exp).ok()),
)
});

row.add_cell(access);

let refresh = state
.refresh_token
.as_ref()
.map(|refresh| {
self.token::<_, RefreshTokenClaims>(&refresh, |token| {
self.expiration(
token
.exp
.and_then(|exp| OffsetDateTime::from_unix_timestamp(exp).ok()),
)
})
})
.unwrap_or_else(|| Cell::new(""));

row.add_cell(refresh);
}

table.add_row(row);
Expand All @@ -73,4 +87,50 @@ impl List {

Ok(())
}

/// Decode a token and call the function to extract cell information
///
/// NOTE: The token is not being verified.
fn token<F, T>(&self, token: &str, f: F) -> Cell
where
F: FnOnce(T) -> Cell,
T: CompactJson,
{
let token = match Compact::<T, Empty>::new_encoded(&token).unverified_payload() {
Ok(token) => token,
Err(err) => return Cell::new(err.to_string()).fg(Color::Red),
};

f(token)
}

fn expiration(&self, expires: Option<OffsetDateTime>) -> Cell {
match expires {
None => "∞".into(),
Some(expires) => {
let rem = expires - OffsetDateTime::now_utc();

// truncate to seconds
let format_rem = humantime::Duration::from(std::time::Duration::from_secs(
rem.unsigned_abs().as_secs(),
));

let details = match self.details {
true => match expires.format(format_description!(
" ([year]-[month]-[day] [hour]:[minute]:[second]Z)"
)) {
Ok(format) => format,
Err(err) => return Cell::new(err.to_string()).fg(Color::Red),
},
false => "".into(),
};

if rem.is_positive() {
Cell::new(format!("valid: {format_rem}{details}")).fg(Color::Green)
} else {
Cell::new(format!("expired: {format_rem}{details}")).fg(Color::DarkGrey)
}
}
}
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]

mod claims;
mod cmd;
mod config;
mod http;
Expand Down

0 comments on commit c8d611c

Please sign in to comment.