Skip to content

Commit

Permalink
Adding `--date' argument to configure time format (#235)
Browse files Browse the repository at this point in the history
* Adding `--date' argument to configure time format

--iso8601 was too hard for me to remember, and most often than not I want to see the time in my local timezone

* Remove `--iso8601` in favor of `--date`
  • Loading branch information
jazcarate authored Feb 16, 2023
1 parent b1cbb37 commit 4b0dfa3
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add Macports installation info #231
- Remove Gofish installation info. See #228
- Adds support for EdDSA algo
- Added `--date` argument to change the display format of the timestamps

# 5.0.3

Expand Down
51 changes: 46 additions & 5 deletions src/cli_config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::translators::{PayloadItem, SupportedTypes};
use crate::translators::{PayloadItem, SupportedTypes, TimeFormat};
use crate::utils::parse_duration_string;
use chrono::format::{parse, Parsed, StrftimeItems};
use clap::{Parser, Subcommand, ValueEnum};
use jsonwebtoken::Algorithm;
use std::path::PathBuf;
Expand Down Expand Up @@ -116,10 +117,15 @@ pub struct DecodeArgs {
#[clap(value_parser)]
pub algorithm: SupportedAlgorithms,

/// Display unix timestamps as ISO 8601 dates
#[clap(long = "iso8601")]
#[clap(value_parser)]
pub iso_dates: bool,
/// Display unix timestamps as ISO 8601 dates [default: UTC] [possible values: UTC, Local, Offset (e.g. -02:00)]
#[clap(long = "date")]
#[clap(aliases = &["dates", "time"])]
#[clap(num_args = 0..=1)]
#[clap(require_equals = true)]
#[clap(value_parser = time_format)]
#[clap(default_value = None)]
#[clap(default_missing_value = "UTC")]
pub time_format: Option<TimeFormat>,

/// The secret to validate the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes
#[clap(long = "secret", short = 'S')]
Expand Down Expand Up @@ -187,6 +193,25 @@ fn is_timestamp_or_duration(val: &str) -> Result<String, String> {
}
}

fn time_format(arg: &str) -> Result<TimeFormat, String> {
match arg.to_uppercase().as_str() {
"UTC" => Ok(TimeFormat::UTC),
"LOCAL" => Ok(TimeFormat::Local),
_ => {
let mut parsed = Parsed::new();
match parse(&mut parsed, arg, StrftimeItems::new("%#z")) {
Ok(_) => match parsed.offset {
Some(offset) => Ok(TimeFormat::Fixed(offset)),
None => panic!("Should have been able to parse the offset"),
},
Err(_) => Err(String::from(
"must be one of `Local`, `UTC` or an offset (-02:00)",
)),
}
}
}
}

pub fn translate_algorithm(alg: &SupportedAlgorithms) -> Algorithm {
match alg {
SupportedAlgorithms::HS256 => Algorithm::HS256,
Expand Down Expand Up @@ -235,4 +260,20 @@ mod tests {
assert!(is_timestamp_or_duration("2398ybdfiud93").is_err());
assert!(is_timestamp_or_duration("1 day -1 hourz").is_err());
}

#[test]
fn is_valid_time_format() {
assert_eq!(time_format("local"), Ok(TimeFormat::Local));
assert_eq!(time_format("LoCaL"), Ok(TimeFormat::Local));
assert_eq!(time_format("utc"), Ok(TimeFormat::UTC));
assert_eq!(time_format("+03:00"), Ok(TimeFormat::Fixed(10800)));
assert_eq!(time_format("+03:30"), Ok(TimeFormat::Fixed(12600)));
}

#[test]
fn is_invalid_time_format() {
assert!(time_format("yolo").is_err());
assert!(time_format("2398ybdfiud93").is_err());
assert!(time_format("+3").is_err());
}
}
27 changes: 24 additions & 3 deletions src/translators.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::utils::parse_duration_string;
use chrono::{TimeZone, Utc};
use chrono::{FixedOffset, Local, TimeZone, Utc};
use clap::ValueEnum;
use serde_derive::{Deserialize, Serialize};
use serde_json::{from_str, Value};
Expand All @@ -20,6 +20,16 @@ pub enum SupportedTypes {
JWT,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeFormat {
/// Displays UTC (+00:00)
UTC,
/// Displays your local timezone
Local,
/// Displays a fixed timezone
Fixed(i32),
}

impl PayloadItem {
pub fn from_string_with_name(val: Option<&String>, name: &str) -> Option<PayloadItem> {
match val {
Expand Down Expand Up @@ -65,13 +75,24 @@ impl Payload {
Payload(payload)
}

pub fn convert_timestamps(&mut self) {
pub fn convert_timestamps(&mut self, offset: TimeFormat) {
let timestamp_claims: Vec<String> = vec!["iat".into(), "nbf".into(), "exp".into()];

for (key, value) in self.0.iter_mut() {
if timestamp_claims.contains(key) && value.is_number() {
*value = match value.as_i64() {
Some(timestamp) => Utc.timestamp_opt(timestamp, 0).unwrap().to_rfc3339().into(),
Some(timestamp) => match offset {
TimeFormat::UTC => Utc.timestamp_opt(timestamp, 0).unwrap().to_rfc3339(),
TimeFormat::Local => {
Local.timestamp_opt(timestamp, 0).unwrap().to_rfc3339()
}
TimeFormat::Fixed(secs) => FixedOffset::east_opt(secs)
.unwrap()
.timestamp_opt(timestamp, 0)
.unwrap()
.to_rfc3339(),
}
.into(),
None => value.clone(),
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/translators/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,10 @@ pub fn decode_token(
let token_data = decode::<Payload>(&jwt, &insecure_decoding_key, &insecure_validator)
.map_err(jsonwebtoken::errors::Error::into)
.map(|mut token| {
if arguments.iso_dates {
token.claims.convert_timestamps();
if arguments.time_format.is_some() {
token
.claims
.convert_timestamps(arguments.time_format.unwrap_or(super::TimeFormat::UTC));
}

token
Expand Down
147 changes: 144 additions & 3 deletions tests/main_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ mod tests {
decode_token, print_decoded_token, OutputFormat, TokenOutput,
};
use super::translators::encode::{encode_token, print_encoded_token};
use super::translators::TimeFormat;
use super::utils::slurp_file;
use chrono::{Duration, TimeZone, Utc};
use chrono::{Duration, FixedOffset, Local, TimeZone, Utc};
use clap::{CommandFactory, FromArgMatches};
use jsonwebtoken::{Algorithm, TokenData};
use serde_json::{from_value, Result as JsonResult};
use tempdir::TempDir;

const HOUR: i32 = 3600;

#[test]
fn encodes_a_token() {
let exp = (Utc::now() + Duration::minutes(60)).timestamp();
Expand Down Expand Up @@ -697,7 +700,7 @@ mod tests {
}

#[test]
fn shows_timestamps_as_iso_dates() {
fn shows_timestamps_as_dates() {
let exp = (Utc::now() + Duration::minutes(60)).timestamp();
let nbf = Utc::now().timestamp();
let encode_matcher = App::command()
Expand All @@ -720,7 +723,7 @@ mod tests {
"decode",
"-S",
"1234567890",
"--iso8601",
"--date",
&encoded_token,
])
.unwrap();
Expand Down Expand Up @@ -842,4 +845,142 @@ mod tests {
assert_eq!(payload.0["nbf"], nbf);
assert_eq!(payload.0["exp"], exp);
}

#[test]
fn shows_timestamps_as_dates_with_local_offset() {
let exp = (Utc::now() + Duration::minutes(60)).timestamp();
let nbf = Utc::now().timestamp();
let encode_matcher = App::command()
.try_get_matches_from(vec![
"jwt",
"encode",
&format!("--exp={}", &exp.to_string()),
"--nbf",
&nbf.to_string(),
"-S",
"1234567890",
])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encode_arguments = EncodeArgs::from_arg_matches(encode_matches).unwrap();
let encoded_token = encode_token(&encode_arguments).unwrap();
let decode_matcher = App::command()
.try_get_matches_from(vec![
"jwt",
"decode",
"-S",
"1234567890",
"--date=local",
&encoded_token,
])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap();
let (decoded_token, token_data, _) = decode_token(&decode_arguments);

assert!(decoded_token.is_ok());

let TokenData { claims, header: _ } = token_data.unwrap();

assert!(claims.0.get("iat").is_some());
assert!(claims.0.get("nbf").is_some());
assert!(claims.0.get("exp").is_some());
assert_eq!(
claims.0.get("iat"),
Some(&Local.timestamp_opt(nbf, 0).unwrap().to_rfc3339().into())
);
assert_eq!(
claims.0.get("nbf"),
Some(&Local.timestamp_opt(nbf, 0).unwrap().to_rfc3339().into())
);
assert_eq!(
claims.0.get("exp"),
Some(&Local.timestamp_opt(exp, 0).unwrap().to_rfc3339().into())
);
}

#[test]
fn shows_timestamps_as_dates_with_fixed_offset() {
let exp = (Utc::now() + Duration::minutes(60)).timestamp();
let nbf = Utc::now().timestamp();
let encode_matcher = App::command()
.try_get_matches_from(vec![
"jwt",
"encode",
&format!("--exp={}", &exp.to_string()),
"--nbf",
&nbf.to_string(),
"-S",
"1234567890",
])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encode_arguments = EncodeArgs::from_arg_matches(encode_matches).unwrap();
let encoded_token = encode_token(&encode_arguments).unwrap();
let decode_matcher = App::command()
.try_get_matches_from(vec![
"jwt",
"decode",
"-S",
"1234567890",
"--dates=+03:00",
&encoded_token,
])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap();
let (decoded_token, token_data, _) = decode_token(&decode_arguments);

assert!(decoded_token.is_ok());

let TokenData { claims, header: _ } = token_data.unwrap();

assert!(claims.0.get("iat").is_some());
assert!(claims.0.get("nbf").is_some());
assert!(claims.0.get("exp").is_some());
assert_eq!(
claims.0.get("iat"),
Some(
&FixedOffset::east_opt(3 * HOUR)
.unwrap()
.timestamp_opt(nbf, 0)
.unwrap()
.to_rfc3339()
.into()
)
);
assert_eq!(
claims.0.get("nbf"),
Some(
&FixedOffset::east_opt(3 * HOUR)
.unwrap()
.timestamp_opt(nbf, 0)
.unwrap()
.to_rfc3339()
.into()
)
);
assert_eq!(
claims.0.get("exp"),
Some(
&FixedOffset::east_opt(3 * HOUR)
.unwrap()
.timestamp_opt(exp, 0)
.unwrap()
.to_rfc3339()
.into()
)
);
}

#[test]
fn parses_date_format_with_no_equals() {
let decode_matcher = App::command()
.try_get_matches_from(vec!["jwt", "decode", "--date", "some token"])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap();

assert_eq!(decode_arguments.time_format, Some(TimeFormat::UTC));
}
}

0 comments on commit 4b0dfa3

Please sign in to comment.