Skip to content

Commit

Permalink
feat: don't add exp claim automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-engel committed Feb 14, 2021
1 parent a5d5d43 commit 5e05b90
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
- Remove the `prn` option as it's not included in the spec any longer #114
- Trim whitespace around jwt before encoding #120
- Support adding `jti` when encoding
- Add `no-iat` flag to disable automatic `iat` claim generation
- Avoid adding an `exp` claim automatically. Instead, the `--exp` flag must be present, with or without a value

# 3.3.0

Expand Down
31 changes: 24 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ fn config_options<'a, 'b>() -> App<'a, 'b> {
.long("nbf")
.short("n")
.validator(is_timestamp_or_duration),
).arg(
Arg::with_name("no_iat")
.help("prevent an iat claim from being automatically added")
.long("no-iat")
).arg(
Arg::with_name("secret")
.help("the secret to sign the JWT with. Can be prefixed with @ to read from a binary file")
Expand Down Expand Up @@ -406,10 +410,16 @@ fn encode_token(matches: &ArgMatches) -> JWTResult<String> {
_ => panic!("Invalid JSON provided!"),
});
let now = Utc::now().timestamp();
let expires = PayloadItem::from_timestamp_with_name(matches.value_of("expires"), "exp", now);
let expires = match matches.occurrences_of("expires") {
0 => None,
_ => PayloadItem::from_timestamp_with_name(matches.value_of("expires"), "exp", now),
};
let not_before =
PayloadItem::from_timestamp_with_name(matches.value_of("not_before"), "nbf", now);
let issued_at = PayloadItem::from_timestamp_with_name(Some(&now.to_string()), "iat", now);
let issued_at = match matches.is_present("no_iat") {
true => None,
false => PayloadItem::from_timestamp_with_name(Some(&now.to_string()), "iat", now),
};
let issuer = PayloadItem::from_string_with_name(matches.value_of("issuer"), "iss");
let subject = PayloadItem::from_string_with_name(matches.value_of("subject"), "sub");
let audience = PayloadItem::from_string_with_name(matches.value_of("audience"), "aud");
Expand Down Expand Up @@ -507,6 +517,12 @@ fn print_decoded_token(
token_data: JWTResult<TokenData<Payload>>,
format: OutputFormat,
) {
let should_validate_exp = if let Ok(token) = &token_data {
token.claims.0.contains_key("exp")
} else {
false
};

if let Err(err) = &validated_token {
match err.kind() {
ErrorKind::InvalidToken => {
Expand All @@ -528,10 +544,8 @@ fn print_decoded_token(
.paint("The secret provided isn't a valid ECDSA key",)
),
ErrorKind::ExpiredSignature => {
if let Ok(token) = &token_data {
if token.claims.0.contains_key("exp") {
println!("{}", Red.bold().paint("The token has expired"))
}
if should_validate_exp {
println!("{}", Red.bold().paint("The token has expired"))
}
}
ErrorKind::InvalidIssuer => {
Expand Down Expand Up @@ -581,7 +595,10 @@ fn print_decoded_token(
}

exit(match validated_token {
Err(_) => 1,
Err(err) => match (err.kind(), should_validate_exp) {
(ErrorKind::ExpiredSignature, false) => 0,
_ => 1,
},
Ok(_) => 0,
})
}
Expand Down
86 changes: 81 additions & 5 deletions tests/jwt-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ mod tests {
}

#[test]
fn adds_iat_exp_automatically() {
fn adds_iat_automatically() {
let encode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "encode", "-S", "1234567890"])
.get_matches_from_safe(vec!["jwt", "encode", "--exp", "-S", "1234567890"])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encoded_token = encode_token(&encode_matches).unwrap();
Expand All @@ -266,14 +266,80 @@ mod tests {

let TokenData { claims, header: _ } = decoded_token.unwrap();
let iat = from_value::<i64>(claims.0["iat"].clone());
let exp = from_value::<i64>(claims.0["exp"].clone());

assert!(iat.is_ok());
assert!(exp.is_ok());
assert!(iat.unwrap().is_positive());
}

#[test]
fn stops_exp_from_automatically_being_added() {
let encode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "encode", "-S", "1234567890"])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encoded_token = encode_token(&encode_matches).unwrap();
let decode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let (decoded_token, token_data, _) = decode_token(&decode_matches);

assert!(decoded_token.is_err());

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

assert!(claims.0.get("exp").is_none());
}

#[test]
fn adds_default_exp_automatically() {
let encode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "encode", "--exp", "-S", "1234567890"])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encoded_token = encode_token(&encode_matches).unwrap();
let decode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let (decoded_token, _, _) = decode_token(&decode_matches);

assert!(decoded_token.is_ok());

let TokenData { claims, header: _ } = decoded_token.unwrap();
let exp = from_value::<i64>(claims.0["exp"].clone());

assert!(exp.is_ok());
assert!(exp.unwrap().is_positive());
}

#[test]
fn stops_iat_from_automatically_being_added() {
let encode_matcher = config_options()
.get_matches_from_safe(vec![
"jwt",
"encode",
"--no-iat",
"--exp",
"-S",
"1234567890",
])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encoded_token = encode_token(&encode_matches).unwrap();
let decode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
.unwrap();
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
let (decoded_token, _, _) = decode_token(&decode_matches);

assert!(decoded_token.is_ok());

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

assert!(claims.0.get("iat").is_none());
}

#[test]
fn allows_for_a_custom_exp() {
let exp = (Utc::now() + Duration::minutes(60)).timestamp();
Expand Down Expand Up @@ -342,7 +408,15 @@ mod tests {
#[test]
fn allows_for_nbf_as_systemd_string() {
let encode_matcher = config_options()
.get_matches_from_safe(vec!["jwt", "encode", "-S", "1234567890", "-n", "+5 min"])
.get_matches_from_safe(vec![
"jwt",
"encode",
"-S",
"1234567890",
"--exp",
"-n",
"+5 min",
])
.unwrap();
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
let encoded_token = encode_token(&encode_matches).unwrap();
Expand Down Expand Up @@ -493,6 +567,7 @@ mod tests {
"encode",
"-A",
"RS256",
"--exp",
"-S",
"@./tests/private_rsa_key.der",
&body,
Expand Down Expand Up @@ -526,6 +601,7 @@ mod tests {
"encode",
"-A",
"ES256",
"--exp",
"-S",
"@./tests/private_ecdsa_key.pk8",
&body,
Expand Down

0 comments on commit 5e05b90

Please sign in to comment.