include!("../src/main.rs"); #[cfg(test)] mod tests { use super::cli_config::{App, DecodeArgs, EncodeArgs}; use super::translators::decode::{ 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, 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(); let nbf = Utc::now().timestamp(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-S", "1234567890", "-A", "HS256", "-a", "yolo", &format!("-e={}", &exp.to_string()), "-i", "yolo-service", "-k", "1234", "-n", &nbf.to_string(), "--jti", "yolo-jti", "-P", "this=that", "-P", "number=10", "-P", "array=[1, 2, 3]", "-P", "object={\"foo\": \"bar\"}", "-s", "yolo-subject", "{\"test\":\"json value\",\"bool\":true,\"json_number\":1}", ]) .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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header } = decoded_token.unwrap(); assert_eq!(header.alg, Algorithm::HS256); assert_eq!(header.kid, Some("1234".to_string())); assert_eq!(claims.0["aud"], "yolo"); assert_eq!(claims.0["iss"], "yolo-service"); assert_eq!(claims.0["sub"], "yolo-subject"); assert_eq!(claims.0["nbf"], nbf); assert_eq!(claims.0["exp"], exp); assert_eq!(claims.0["jti"], "yolo-jti"); assert_eq!(claims.0["this"], "that"); assert_eq!(claims.0["test"], "json value"); assert_eq!(claims.0["bool"], true); assert_eq!(claims.0["json_number"], 1); assert_eq!(claims.0["number"], 10); assert_eq!(claims.0["array"].to_string(), "[1,2,3]"); assert_eq!(claims.0["object"]["foo"], "bar"); } #[test] fn adds_iat_automatically() { let encode_matcher = App::command() .try_get_matches_from(vec!["jwt", "encode", "--exp", "-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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header: _ } = decoded_token.unwrap(); let iat = from_value::(claims.0["iat"].clone()); assert!(iat.is_ok()); assert!(iat.unwrap().is_positive()); } #[test] fn stops_exp_from_automatically_being_added() { let encode_matcher = App::command() .try_get_matches_from(vec!["jwt", "encode", "-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", &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_err()); let TokenData { claims, header: _ } = token_data.unwrap(); assert!(claims.0.get("exp").is_none()); } #[test] fn adds_default_exp_automatically() { let encode_matcher = App::command() .try_get_matches_from(vec!["jwt", "encode", "--exp", "-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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header: _ } = decoded_token.unwrap(); let exp = from_value::(claims.0["exp"].clone()); assert!(exp.is_ok()); assert!(exp.unwrap().is_positive()); } #[test] fn stops_iat_from_automatically_being_added() { let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "--no-iat", "--exp", "-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", &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, _, _) = decode_token(&decode_arguments); 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(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-S", "1234567890", &format!("-e={}", &exp.to_string()), ]) .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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header: _ } = decoded_token.unwrap(); let exp_claim = from_value::(claims.0["exp"].clone()); assert!(exp_claim.is_ok()); assert_eq!(exp_claim.unwrap(), exp); } #[test] fn allows_for_a_no_typ() { let exp = (Utc::now() + Duration::minutes(60)).timestamp(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "--no-typ", "-S", "1234567890", &format!("-e={}", &exp.to_string()), ]) .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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims: _, header } = decoded_token.unwrap(); assert!(header.typ.is_none()); } #[test] fn returns_error_when_exp_is_not_set() { let encode_matcher = App::command() .try_get_matches_from(vec!["jwt", "encode", "-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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_err()); } #[test] fn returns_no_error_when_ignore_exp_parameter_is_set() { let encode_matcher = App::command() .try_get_matches_from(vec!["jwt", "encode", "-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", "--ignore-exp", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); } #[test] fn allows_for_a_custom_exp_as_systemd_string() { let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-S", "1234567890", "-e=+10 min -30 sec", ]) .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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header: _ } = decoded_token.unwrap(); let exp_claim = from_value::(claims.0["exp"].clone()); let iat_claim = from_value::(claims.0["iat"].clone()); assert!(iat_claim.is_ok()); let iat = iat_claim.unwrap(); assert!(exp_claim.is_ok()); let exp = exp_claim.unwrap(); assert!(iat.is_positive()); assert!(exp.is_positive()); assert_eq!(exp - iat, (10 * 60 - 30)); } #[test] fn allows_for_nbf_as_systemd_string() { let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-S", "1234567890", "--exp", "-n", "+5 min", ]) .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", &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, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let TokenData { claims, header: _ } = decoded_token.unwrap(); let nbf_claim = from_value::(claims.0["nbf"].clone()); let iat_claim = from_value::(claims.0["iat"].clone()); assert!(iat_claim.is_ok()); let iat = iat_claim.unwrap(); assert!(nbf_claim.is_ok()); let nbf = nbf_claim.unwrap(); assert!(iat.is_positive()); assert!(nbf.is_positive()); assert_eq!(nbf - iat, (5 * 60)); } #[test] fn decodes_a_token() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE4OTM0NTYwMDAsImlhdCI6MTU0MjQ5MjMxMywidGhpcyI6InRoYXQifQ.YTWit46_AEMMVv0P48NeJJIqXmMHarGjfRxtR7jLlxE", "-S", "1234567890", "-A", "HS256", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_a_token_as_json() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aGlzIjoidGhhdCJ9.AdAECLE_4iRa0uomMEdsMV2hDXv1vhLpym567-AzhrM", "-j", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, format) = decode_token(&decode_arguments); assert!(result.is_ok()); assert!(format == OutputFormat::Json); } #[test] fn decodes_a_token_with_invalid_secret() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aGlzIjoidGhhdCJ9.AdAECLE_4iRa0uomMEdsMV2hDXv1vhLpym567-AzhrM", "-S", "yolo", "-A", "HS256", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_err()); } #[test] fn decodes_a_token_without_a_secret() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aGlzIjoidGhhdCJ9.AdAECLE_4iRa0uomMEdsMV2hDXv1vhLpym567-AzhrM", "-A", "HS256", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_a_token_without_an_alg() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aGlzIjoidGhhdCJ9.AdAECLE_4iRa0uomMEdsMV2hDXv1vhLpym567-AzhrM", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_a_token_without_a_typ() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SEQijh6tEuOOAAKpHPuKxgFqEvlTNP1jj4FUNoBwXaM", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_a_token_with_leading_and_trailing_whitespace() { let matches = App::command() .try_get_matches_from(vec![ "jwt", "decode", " eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SEQijh6tEuOOAAKpHPuKxgFqEvlTNP1jj4FUNoBwXaM ", ]) .unwrap(); let decode_matches = matches.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_hmac_token_using_jwks() { let jwks = r#"{"kty":"oct","k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"}"#; let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "HS256", "--exp", "-S", jwks, &body, ]) .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", jwks, "-A", "HS256", "--ignore-exp", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_rsa_ssa_pkcs1_v1_5_token_using_key_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "RS256", "--exp", "-S", "@./tests/private_rsa_key.der", &body, ]) .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", "@./tests/public_rsa_key.der", "-A", "RS256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } // EncodingKey doesn't implement `debug` or `eq`, so we can't run these tests for now // #[test] // fn encoding_key_from_secret_handles_at() { // let expected = EncodingKey::from_secret(include_bytes!("hmac-key.bin")); // let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap(); // assert_eq!(expected, key); // } // #[test] // fn encoding_key_from_secret_handles_base64() { // let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; // let arg = format!("b64:{}", b64); // let expected = EncodingKey::from_secret(&base64_engine.decode(b64).unwrap()); // let key = encoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); // assert_eq!(expected, key); // } // #[test] // fn decoding_key_from_secret_handles_at() { // let expected = DecodingKey::from_secret(include_bytes!("hmac-key.bin")); // let key = decoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap(); // assert_eq!(expected, key); // } // #[test] // fn decoding_key_from_secret_handles_base64() { // let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; // let arg = format!("b64:{}", b64); // let expected = DecodingKey::from_secret(&base64_engine.decode(b64).unwrap()).into_static(); // let key = decoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); // assert_eq!(expected, key); // } #[test] fn encodes_and_decodes_an_rsa_ssa_pss_token_using_key_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "PS256", "--exp", "-S", "@./tests/private_rsa_key.der", &body, ]) .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(); println!("enc {encoded_token}"); let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", "@./tests/public_rsa_key.der", "-A", "PS256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_an_rsa_ssa_pss_token_using_key_from_file() { let token: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJmaWVsZCI6InZhbHVlIiwiaWF0IjoxNjI1OTMxNjAwLCJleHAiOjkwMDAwMDAwMDB9.Tt1siDczvVAi89dH8QqTZ_n5Ejz4gAIzVLqucWN5tEqdAVRdWgP8psuRFdC8RKIn1Lp4OsUkAA7NJ79cZt32Eewy84hTYrCgZZ9mcWg5IfXPHcZmTUm6qSyKqANdsnRWThbG3IJSX1D6obI5Y91NhVI5PTRg8sFlDAXaNN9ZVTmAtZXj0b5-MgsjiRqWMW3xi9xQqTxvb5VN37Oot-KDWZXjkO022ixshzFWu8Jt582uMD4qYRp1d0VldgyGO_viDqqk8qTqNA7soUKWyDds0emuecE_bDMeELMfxMR-A1pQeu3FgEhliazIAdXJMNlwRuJG8znLNqCK1nB2Nd8sUQ".to_string(); let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", "@./tests/public_rsa_key.der", "-A", "PS512", &token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn encode_and_decodes_an_rsa_token_using_jwks_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "RS256", "--kid", "2caFcPx-aXaC6SevhV79UDIrs8LgUok2xo0A6DJPqJo", "--exp", "-S", "@./tests/private_rsa_key.der", &body, ]) .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", "@./tests/public_rsa_jwks.json", "-A", "RS256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn decodes_an_rsa_ssa_pss_token_using_jwks_secret() { let jwks = r#"{"keys":[{"use":"sig","kty":"RSA","kid":"2caFcPx-aXaC6SevhV79UDIrs8LgUok2xo0A6DJPqJo","n":"589r2P-JpeFPkH2T8-SBw7ttzHPPlVzqJwb_fcXJl8MGZ_7Jkt8k58Ukgp3cgRdChDNlnrFeXu1wSwU47Mf_o9bBLVQbNCJ7uL-vQYdFwzEipqHusywJ-Qm5qpJyWO5f2hXMHnomZ1KZW4isg7g1kvynUznlSwU25wNUvRurRImxigT2ohmZzHf37n51zyzci5JZxneOojcyfXdhDWtRGuSbREW3XZqKnJbUOK9HqosrgidbFZil3j2uf4br7DLtdlZMJ4JzTE_ZX273el_uv_XFg-OuHvgdBHtgzN9rkKapkPyUT0BsWfOPyjEtrjzdAAiFQfuwhwIWQPidzBUKtw","e":"AQAB"},{"use":"enc","kty":"RSA","kid":"2caFcPx-aXaC6SevhV79UDIrs8LgUok2xo0A6DJPqJo","n":"589r2P-JpeFPkH2T8-SBw7ttzHPPlVzqJwb_fcXJl8MGZ_7Jkt8k58Ukgp3cgRdChDNlnrFeXu1wSwU47Mf_o9bBLVQbNCJ7uL-vQYdFwzEipqHusywJ-Qm5qpJyWO5f2hXMHnomZ1KZW4isg7g1kvynUznlSwU25wNUvRurRImxigT2ohmZzHf37n51zyzci5JZxneOojcyfXdhDWtRGuSbREW3XZqKnJbUOK9HqosrgidbFZil3j2uf4br7DLtdlZMJ4JzTE_ZX273el_uv_XFg-OuHvgdBHtgzN9rkKapkPyUT0BsWfOPyjEtrjzdAAiFQfuwhwIWQPidzBUKtw","e":"AQAB"}]}"#; let token: String = "eyJ0eXAiOiJKV1QiLCJraWQiOiIyY2FGY1B4LWFYYUM2U2V2aFY3OVVESXJzOExnVW9rMnhvMEE2REpQcUpvIiwiYWxnIjoiUFM1MTIifQ.eyJmaWVsZCI6InZhbHVlIiwiZm9vIjoiYmFyIn0.O6r-pK6rDw0BAadqJmBivtjk7ELU2pYpKIOU7qD8rah9mzwm29A0KoCoOabtQCkKNcmlcIKoC812UrP_nDZrAsC1msHPfjvkKlbkX63_zEcRCv-6VC1FMuek8yY6mhKiFaTISPDBfHCg_Fru2BDar_qBJn8rtct9y6cgDA5vLvL81jLmJrCXW8C5wP9xrkG5CUXdW9A8fqtxcEDoNZoYUoxCnLkh3Pz5IfAluepqDYjj6kvMWuAC88K1B_a1Z8QTqCuJZNIj_5g6UExmK7pqKvB5RZo62KGTw8wWqkmaPTf4TnD4n3Rb1K-MN1LTWMySqgPaw5YlSxT2eFwDvhRBnA".to_string(); let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", jwks, "-A", "PS512", "--ignore-exp", &token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn returns_error_when_file_format_is_wrong_during_encode() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "PS256", "--exp", "-S", "./tests/private_rsa_key.der", &body, ]) .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); assert!(encoded_token.is_err()); } #[test] fn returns_error_when_file_format_is_wrong_during_decode() { let token: String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJmaWVsZCI6InZhbHVlIiwiaWF0IjoxNjI1OTMxNjAwLCJleHAiOjkwMDAwMDAwMDB9.Tt1siDczvVAi89dH8QqTZ_n5Ejz4gAIzVLqucWN5tEqdAVRdWgP8psuRFdC8RKIn1Lp4OsUkAA7NJ79cZt32Eewy84hTYrCgZZ9mcWg5IfXPHcZmTUm6qSyKqANdsnRWThbG3IJSX1D6obI5Y91NhVI5PTRg8sFlDAXaNN9ZVTmAtZXj0b5-MgsjiRqWMW3xi9xQqTxvb5VN37Oot-KDWZXjkO022ixshzFWu8Jt582uMD4qYRp1d0VldgyGO_viDqqk8qTqNA7soUKWyDds0emuecE_bDMeELMfxMR-A1pQeu3FgEhliazIAdXJMNlwRuJG8znLNqCK1nB2Nd8sUQ".to_string(); let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", "./tests/public_rsa_key.der", "-A", "PS512", &token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_err()); } #[test] fn encodes_and_decodes_an_ecdsa_token_using_key_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "ES256", "--exp", "-S", "@./tests/private_ecdsa_key.pk8", &body, ]) .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", "@./tests/public_ecdsa_key.pk8", "-A", "ES256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); dbg!(&result); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_ecdsa_token_using_jwks_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "ES256", "--kid", "4h7wt2IHHu_RLR6OtlZjCe_mIt8xAReS0cDEwwWAeKU", "--exp", "-S", "@./tests/private_ecdsa_key.pk8", &body, ]) .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", "@./tests/public_ecdsa_jwks.json", "-A", "ES256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); dbg!(&result); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_ec_token_using_jwks_secret() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "ES256", "--kid", "4h7wt2IHHu_RLR6OtlZjCe_mIt8xAReS0cDEwwWAeKU", "--exp", "-S", "@./tests/private_ecdsa_key.pk8", &body, ]) .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(); // "$(cat ./tests/pub_ecdsa_jwks.json)" let jwks = r#"{"keys":[{"use":"sig","kty":"EC","kid":"4h7wt2IHHu_RLR6OtlZjCe_mIt8xAReS0cDEwwWAeKU","crv":"P-256","x":"w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ","y":"wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4"},{"use":"enc","kty":"EC","kid":"4h7wt2IHHu_RLR6OtlZjCe_mIt8xAReS0cDEwwWAeKU","crv":"P-256","x":"w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ","y":"wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4"}]} "#; let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", jwks, "-A", "ES256", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); dbg!(&result); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_eddsa_token_using_key_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "EDDSA", "--exp", "-S", "@./tests/private_eddsa_key.pem", &body, ]) .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", "@./tests/public_eddsa_key.pem", "-A", "EDDSA", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } #[test] fn encodes_and_decodes_an_eddsa_token_using_jwks_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "-A", "EDDSA", "--kid", "4h7wt2IHHu_RLR6OtlZjCe_mIt8xAReS0cDEwwWAeKU", "--exp", "-S", "@./tests/private_eddsa_key.pem", &body, ]) .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", "@./tests/public_eddsa_jwks.json", "-A", "EDDSA", &encoded_token, ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (result, _, _) = decode_token(&decode_arguments); assert_eq!(result.err(), None); } #[test] fn shows_timestamps_as_dates() { 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", &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(&Utc.timestamp_opt(nbf, 0).unwrap().to_rfc3339().into()) ); assert_eq!( claims.0.get("nbf"), Some(&Utc.timestamp_opt(nbf, 0).unwrap().to_rfc3339().into()) ); assert_eq!( claims.0.get("exp"), Some(&Utc.timestamp_opt(exp, 0).unwrap().to_rfc3339().into()) ); } #[test] fn writes_output_to_file() { let tmp_dir_result = TempDir::new("jwtclitest"); assert!(tmp_dir_result.is_ok()); let tmp_dir = tmp_dir_result.unwrap(); let out_path = tmp_dir.path().join("jwt.out"); println!("jwt output path: {}", out_path.to_str().unwrap()); let secret = "1234567890"; let kid = "1234"; 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", "-S", secret, "-A", "HS256", &format!("-e={}", &exp.to_string()), "-k", kid, "-n", &nbf.to_string(), "-o", out_path.to_str().unwrap(), ]) .unwrap(); let encode_matches = encode_matcher.subcommand_matches("encode").unwrap(); let encode_arguments = EncodeArgs::from_arg_matches(encode_matches).unwrap(); let out_path_from_args = &encode_arguments.output_path; assert!(out_path_from_args.is_some()); assert_eq!(out_path, *out_path_from_args.as_ref().unwrap()); let encoded_token = encode_token(&encode_arguments); let print_encoded_result = print_encoded_token(encoded_token, out_path_from_args); assert!(print_encoded_result.is_ok()); let out_content_buf = slurp_file(out_path.to_string_lossy().to_string()); let out_content_str = std::str::from_utf8(&out_content_buf); assert!(out_content_str.is_ok()); println!("jwt: {}", out_content_str.unwrap()); let json_path = tmp_dir.path().join("decoded.json"); println!("decoded json path: {}", json_path.to_str().unwrap()); let decode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "decode", "-S", secret, out_content_str.unwrap(), "-o", json_path.to_str().unwrap(), ]) .unwrap(); let decode_matches = decode_matcher.subcommand_matches("decode").unwrap(); let decode_arguments = DecodeArgs::from_arg_matches(decode_matches).unwrap(); let (decoded_token, decoded_token_data, decoded_output_format) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); let json_path_from_args = &decode_arguments.output_path; assert!(json_path_from_args.is_some()); assert_eq!(json_path, *json_path_from_args.as_ref().unwrap()); let json_print_result = print_decoded_token( decoded_token, decoded_token_data, decoded_output_format, json_path_from_args, ); assert!(json_print_result.is_ok()); let json_content_buf = slurp_file(json_path.to_string_lossy().to_string()); let json_content_str = std::str::from_utf8(&json_content_buf); assert!(json_content_str.is_ok()); let json_result: JsonResult = serde_json::from_str(json_content_str.unwrap()); assert!(json_result.is_ok()); let json = json_result.unwrap(); println!("json: {json:#?}"); let TokenOutput { header, payload } = json; assert_eq!(header.alg, Algorithm::HS256); assert_eq!(header.kid, Some(kid.to_string())); 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)); } #[test] fn keeps_payload_order() { let encode_matcher = App::command() .try_get_matches_from(vec![ "jwt", "encode", "--no-iat", "--keep-payload-order", "-S", "1234567890", "--payload", "z=123", "--payload", "a=123", ]) .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(); assert_eq!(encoded_token.as_str(), "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ6IjoxMjMsImEiOjEyM30.kvofE3KpCVQWpvrgx87u9LxjV-AK9bsVmS-Oddbz1Qg") } }