diff --git a/Cargo.lock b/Cargo.lock index da11f84..bb32b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ name = "jwt-cli" version = "6.2.0" dependencies = [ "atty", + "base64 0.22.1", "bunt", "chrono", "clap 4.6.1", diff --git a/Cargo.toml b/Cargo.toml index 66099de..d916a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ test = true [dependencies] clap = { version = "4", features = ["derive"] } jsonwebtoken = "9.3.1" +base64 = "0.22" bunt = "0.2" serde = "1" serde_derive = "1" diff --git a/src/cli_config.rs b/src/cli_config.rs index a241e37..84f9d56 100644 --- a/src/cli_config.rs +++ b/src/cli_config.rs @@ -117,10 +117,11 @@ pub struct EncodeArgs { #[clap(value_parser)] pub no_typ: bool, - /// the secret to sign the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes + /// the secret to sign the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes. + /// Not required when using --alg none. #[clap(long, short = 'S')] #[clap(value_parser)] - pub secret: String, + pub secret: Option, /// The path of the file to write the result to (suppresses default standard output) #[clap(long = "out", short = 'o')] @@ -194,6 +195,9 @@ pub enum SupportedAlgorithms { ES256, ES384, EdDSA, + /// Unsecured JWT (RFC 7519 Section 6.1) with no signature + #[clap(name = "none")] + None, } fn is_payload_item(val: &str) -> Result, String> { @@ -241,20 +245,21 @@ fn time_format(arg: &str) -> Result { } } -pub fn translate_algorithm(alg: &SupportedAlgorithms) -> Algorithm { +pub fn translate_algorithm(alg: &SupportedAlgorithms) -> Option { match alg { - SupportedAlgorithms::HS256 => Algorithm::HS256, - SupportedAlgorithms::HS384 => Algorithm::HS384, - SupportedAlgorithms::HS512 => Algorithm::HS512, - SupportedAlgorithms::RS256 => Algorithm::RS256, - SupportedAlgorithms::RS384 => Algorithm::RS384, - SupportedAlgorithms::RS512 => Algorithm::RS512, - SupportedAlgorithms::PS256 => Algorithm::PS256, - SupportedAlgorithms::PS384 => Algorithm::PS384, - SupportedAlgorithms::PS512 => Algorithm::PS512, - SupportedAlgorithms::ES256 => Algorithm::ES256, - SupportedAlgorithms::ES384 => Algorithm::ES384, - SupportedAlgorithms::EdDSA => Algorithm::EdDSA, + SupportedAlgorithms::HS256 => Some(Algorithm::HS256), + SupportedAlgorithms::HS384 => Some(Algorithm::HS384), + SupportedAlgorithms::HS512 => Some(Algorithm::HS512), + SupportedAlgorithms::RS256 => Some(Algorithm::RS256), + SupportedAlgorithms::RS384 => Some(Algorithm::RS384), + SupportedAlgorithms::RS512 => Some(Algorithm::RS512), + SupportedAlgorithms::PS256 => Some(Algorithm::PS256), + SupportedAlgorithms::PS384 => Some(Algorithm::PS384), + SupportedAlgorithms::PS512 => Some(Algorithm::PS512), + SupportedAlgorithms::ES256 => Some(Algorithm::ES256), + SupportedAlgorithms::ES384 => Some(Algorithm::ES384), + SupportedAlgorithms::EdDSA => Some(Algorithm::EdDSA), + SupportedAlgorithms::None => None, } } diff --git a/src/main.rs b/src/main.rs index 6fdaa6b..eb35292 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,11 +53,11 @@ fn main() { }); } Commands::Decode(arguments) => { - let (validated_token, token_data, format) = decode_token(arguments); + let (validated_token, token_data, format, raw_header) = decode_token(arguments); let output_path = &arguments.output_path; exit( - match print_decoded_token(validated_token, token_data, format, output_path) { + match print_decoded_token(validated_token, token_data, format, output_path, raw_header) { Ok(_) => 0, _ => 1, }, diff --git a/src/translators/decode.rs b/src/translators/decode.rs index 11a32b0..f102618 100644 --- a/src/translators/decode.rs +++ b/src/translators/decode.rs @@ -4,6 +4,7 @@ use crate::utils::{ decoding_key_from_jwks_secret, get_secret_from_file_or_input, write_file, JWTError, JWTResult, SecretType, }; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use jsonwebtoken::errors::ErrorKind; use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Header, TokenData, Validation}; use serde_derive::{Deserialize, Serialize}; @@ -21,17 +22,24 @@ pub enum OutputFormat { #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct TokenOutput { - pub header: Header, + pub header: serde_json::Value, pub payload: Payload, } impl TokenOutput { fn new(data: TokenData) -> Self { TokenOutput { - header: data.header, + header: serde_json::to_value(&data.header).unwrap(), payload: data.claims, } } + + fn new_unsecured(header_json: serde_json::Value, claims: Payload) -> Self { + TokenOutput { + header: header_json, + payload: claims, + } + } } pub fn decoding_key_from_secret( @@ -88,13 +96,82 @@ pub fn decoding_key_from_secret( } } -pub fn decode_token( - arguments: &DecodeArgs, -) -> ( +/// Decode an unsecured JWT (alg: "none") by manually parsing the base64url parts. +/// Returns TokenData with the parsed header and claims. +fn decode_unsecured_token(jwt: &str) -> JWTResult> { + let parts: Vec<&str> = jwt.split('.').collect(); + if parts.len() != 3 { + return Err(JWTError::Internal( + "Invalid JWT: expected 3 parts separated by dots".to_string(), + )); + } + + // The signature part must be empty for unsecured JWTs + if !parts[2].is_empty() { + return Err(JWTError::Internal( + "Unsecured JWT (alg: none) must have an empty signature".to_string(), + )); + } + + let header_bytes = URL_SAFE_NO_PAD + .decode(parts[0]) + .map_err(|e| JWTError::Internal(format!("Invalid base64 in header: {e}")))?; + // Parse the header as a generic JSON object first, since jsonwebtoken::Header + // does not recognize "none" as a valid algorithm variant. + let header_json: serde_json::Value = serde_json::from_slice(&header_bytes) + .map_err(|e| JWTError::Internal(format!("Invalid JSON in header: {e}")))?; + let typ = header_json + .get("typ") + .and_then(|v| v.as_str()) + .map(String::from); + let kid = header_json + .get("kid") + .and_then(|v| v.as_str()) + .map(String::from); + let mut header = Header::new(Algorithm::HS256); // placeholder, overridden below + header.typ = typ; + header.kid = kid; + + let claims_bytes = URL_SAFE_NO_PAD + .decode(parts[1]) + .map_err(|e| JWTError::Internal(format!("Invalid base64 in payload: {e}")))?; + let claims: Payload = serde_json::from_slice(&claims_bytes) + .map_err(|e| JWTError::Internal(format!("Invalid JSON in payload: {e}")))?; + + Ok(TokenData { header, claims }) +} + +/// Check if a JWT header has alg: "none" by peeking at the base64-decoded header. +fn is_unsecured_jwt(jwt: &str) -> bool { + let header_part = jwt.split('.').next().unwrap_or(""); + if let Ok(bytes) = URL_SAFE_NO_PAD.decode(header_part) { + if let Ok(value) = serde_json::from_slice::(&bytes) { + return value.get("alg").and_then(|a| a.as_str()) == Some("none"); + } + } + false +} + +/// Extract the raw header JSON from an unsecured JWT for display purposes. +/// The jsonwebtoken::Header struct cannot represent alg: "none", so we +/// preserve the original header as a serde_json::Value. +fn unsecured_header_json(jwt: &str) -> Option { + let header_part = jwt.split('.').next()?; + let bytes = URL_SAFE_NO_PAD.decode(header_part).ok()?; + serde_json::from_slice(&bytes).ok() +} + +/// Result of decoding a JWT: (validated, display, format, raw_header_override). +/// The raw_header_override is `Some` for unsecured JWTs (alg: "none") because +/// jsonwebtoken::Header cannot represent that algorithm. +pub type DecodeResult = ( JWTResult>, JWTResult>, OutputFormat, -) { + Option, +); + +pub fn decode_token(arguments: &DecodeArgs) -> DecodeResult { let jwt = match arguments.jwt.as_str() { "-" => { let mut buffer = String::new(); @@ -110,10 +187,30 @@ pub fn decode_token( .trim() .to_owned(); + // Handle unsecured JWTs (alg: "none") + if is_unsecured_jwt(&jwt) { + let raw_header = unsecured_header_json(&jwt); + let validated = decode_unsecured_token(&jwt); + let display = decode_unsecured_token(&jwt).map(|mut token| { + if arguments.time_format.is_some() { + token + .claims + .convert_timestamps(arguments.time_format.unwrap_or(super::TimeFormat::UTC)); + } + token + }); + let format = if arguments.json { + OutputFormat::Json + } else { + OutputFormat::Text + }; + return (validated, display, format, raw_header); + } + let header = decode_header(&jwt).ok(); let algorithm = if let Some(alg) = &arguments.algorithm { - translate_algorithm(alg) + translate_algorithm(alg).unwrap_or(Algorithm::HS256) } else { header.as_ref().map(|h| h.alg).unwrap_or(Algorithm::HS256) }; @@ -172,6 +269,7 @@ pub fn decode_token( } else { OutputFormat::Text }, + None, ) } @@ -180,6 +278,7 @@ pub fn print_decoded_token( token_data: JWTResult>, format: OutputFormat, output_path: &Option, + raw_header: Option, ) -> JWTResult<()> { if let Err(err) = &validated_token { match err { @@ -225,16 +324,25 @@ pub fn print_decoded_token( match (output_path.as_ref(), format, token_data) { (Some(path), _, Ok(token)) => { - let json = to_string_pretty(&TokenOutput::new(token)).unwrap(); + let output = match raw_header { + Some(h) => TokenOutput::new_unsecured(h, token.claims), + None => TokenOutput::new(token), + }; + let json = to_string_pretty(&output).unwrap(); write_file(path, json.as_bytes()); println!("Wrote jwt to file {}", path.display()); } (None, OutputFormat::Json, Ok(token)) => { - println!("{}", to_string_pretty(&TokenOutput::new(token)).unwrap()); + let output = match raw_header { + Some(h) => TokenOutput::new_unsecured(h, token.claims), + None => TokenOutput::new(token), + }; + println!("{}", to_string_pretty(&output).unwrap()); } (None, _, Ok(token)) => { + let header_json = raw_header.unwrap_or_else(|| serde_json::to_value(&token.header).unwrap()); bunt::println!("\n{$bold}Token header\n------------{/$}"); - println!("{}\n", to_string_pretty(&token.header).unwrap()); + println!("{}\n", to_string_pretty(&header_json).unwrap()); bunt::println!("{$bold}Token claims\n------------{/$}"); println!("{}", to_string_pretty(&token.claims).unwrap()); } diff --git a/src/translators/encode.rs b/src/translators/encode.rs index bf934d9..f6816e6 100644 --- a/src/translators/encode.rs +++ b/src/translators/encode.rs @@ -2,6 +2,7 @@ use crate::cli_config::{translate_algorithm, EncodeArgs}; use crate::translators::{Claims, Payload, PayloadItem}; use crate::utils::{get_secret_from_file_or_input, write_file, JWTError, JWTResult, SecretType}; use atty::Stream; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use chrono::Utc; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde_json::{from_str, Value}; @@ -67,9 +68,33 @@ pub fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResu } } -pub fn encode_token(arguments: &EncodeArgs) -> JWTResult { - let algorithm = translate_algorithm(&arguments.algorithm); - let header = create_header(algorithm, arguments.kid.as_ref(), arguments.no_typ); +/// Encode an unsecured JWT (alg: "none") per RFC 7519 Section 6.1. +/// The token has no signature: header.payload. (trailing dot, empty signature). +fn encode_unsecured_token(arguments: &EncodeArgs) -> JWTResult { + let claims = build_claims(arguments); + let claims_json = serde_json::to_string(&claims).map_err(|e| { + JWTError::Internal(format!("Failed to serialize claims: {e}")) + })?; + + let mut header_map = serde_json::Map::new(); + header_map.insert("alg".into(), Value::String("none".into())); + if !arguments.no_typ { + header_map.insert("typ".into(), Value::String("JWT".into())); + } + if let Some(kid) = &arguments.kid { + header_map.insert("kid".into(), Value::String(kid.clone())); + } + let header_json = serde_json::to_string(&header_map).map_err(|e| { + JWTError::Internal(format!("Failed to serialize header: {e}")) + })?; + + let header_b64 = URL_SAFE_NO_PAD.encode(header_json.as_bytes()); + let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json.as_bytes()); + + Ok(format!("{header_b64}.{claims_b64}.")) +} + +fn build_claims(arguments: &EncodeArgs) -> Claims { let custom_payloads = arguments.payload.clone(); let custom_payload = arguments .json @@ -114,17 +139,38 @@ pub fn encode_token(arguments: &EncodeArgs) -> JWTResult { maybe_payloads.append(&mut custom_payload.unwrap_or_default()); let payloads = maybe_payloads.into_iter().flatten().collect(); - let claims = match arguments.keep_payload_order { + match arguments.keep_payload_order { true => Claims::OrderKept(payloads), false => { let Payload(_claims) = Payload::from_payloads(payloads); Claims::Reordered(_claims) } - }; + } +} + +pub fn encode_token(arguments: &EncodeArgs) -> JWTResult { + let algorithm = translate_algorithm(&arguments.algorithm); - encoding_key_from_secret(&algorithm, &arguments.secret).and_then(|secret| { - encode(&header, &claims, &secret).map_err(jsonwebtoken::errors::Error::into) - }) + match algorithm { + None => { + // Unsecured JWT (alg: "none") + encode_unsecured_token(arguments) + } + Some(alg) => { + let secret_string = arguments.secret.as_deref().ok_or_else(|| { + JWTError::Internal( + "A secret is required when using a signing algorithm. Use -S to provide one." + .to_string(), + ) + })?; + let header = create_header(alg, arguments.kid.as_ref(), arguments.no_typ); + let claims = build_claims(arguments); + + encoding_key_from_secret(&alg, secret_string).and_then(|secret| { + encode(&header, &claims, &secret).map_err(jsonwebtoken::errors::Error::into) + }) + } + } } pub fn print_encoded_token( diff --git a/tests/main_test.rs b/tests/main_test.rs index 1d74f22..19433a1 100644 --- a/tests/main_test.rs +++ b/tests/main_test.rs @@ -61,7 +61,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -97,7 +97,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -121,7 +121,7 @@ mod tests { .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); + let (decoded_token, token_data, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_err()); @@ -143,7 +143,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -174,7 +174,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -203,7 +203,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -235,7 +235,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -257,7 +257,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_err()); } @@ -282,7 +282,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); } @@ -306,7 +306,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -344,7 +344,7 @@ mod tests { .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); + let (decoded_token, _, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -376,7 +376,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -393,7 +393,7 @@ mod tests { .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); + let (result, _, format, _) = decode_token(&decode_arguments); assert!(result.is_ok()); assert!(format == OutputFormat::Json); @@ -414,7 +414,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_err()); } @@ -432,7 +432,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -448,7 +448,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -464,7 +464,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -480,7 +480,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -511,7 +511,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -547,7 +547,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -617,7 +617,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -638,7 +638,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -676,7 +676,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -699,7 +699,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -742,7 +742,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_err()); } @@ -778,7 +778,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); dbg!(&result); @@ -818,7 +818,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); dbg!(&result); @@ -861,7 +861,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); dbg!(&result); @@ -899,7 +899,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert!(result.is_ok()); } @@ -937,7 +937,7 @@ mod tests { .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); + let (result, _, _, _) = decode_token(&decode_arguments); assert_eq!(result.err(), None); } @@ -972,7 +972,7 @@ mod tests { .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); + let (decoded_token, token_data, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -1057,7 +1057,7 @@ mod tests { .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) = + let (decoded_token, decoded_token_data, decoded_output_format, raw_header) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -1070,6 +1070,7 @@ mod tests { decoded_token_data, decoded_output_format, json_path_from_args, + raw_header, ); assert!(json_print_result.is_ok()); @@ -1083,8 +1084,8 @@ mod tests { println!("json: {json:#?}"); let TokenOutput { header, payload } = json; - assert_eq!(header.alg, Algorithm::HS256); - assert_eq!(header.kid, Some(kid.to_string())); + assert_eq!(header["alg"], "HS256"); + assert_eq!(header["kid"], kid); assert_eq!(payload.0["nbf"], nbf); assert_eq!(payload.0["exp"], exp); } @@ -1119,7 +1120,7 @@ mod tests { .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); + let (decoded_token, token_data, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -1172,7 +1173,7 @@ mod tests { .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); + let (decoded_token, token_data, _, _) = decode_token(&decode_arguments); assert!(decoded_token.is_ok()); @@ -1248,4 +1249,79 @@ mod tests { let encoded_token = encode_token(&encode_arguments).unwrap(); assert_eq!(encoded_token.as_str(), "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ6IjoxMjMsImEiOjEyM30.kvofE3KpCVQWpvrgx87u9LxjV-AK9bsVmS-Oddbz1Qg") } + + #[test] + fn encodes_unsecured_jwt() { + let encode_matcher = App::command() + .try_get_matches_from(vec![ + "jwt", + "encode", + "-A", + "none", + "--no-iat", + "-P", + "role=my-role", + ]) + .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(); + + // Unsecured JWTs must end with a trailing dot (empty signature) + assert!(encoded_token.ends_with('.')); + let parts: Vec<&str> = encoded_token.split('.').collect(); + assert_eq!(parts.len(), 3); + assert!(parts[2].is_empty(), "Signature must be empty for alg: none"); + } + + #[test] + fn decodes_unsecured_jwt() { + // This is the unsecured JWT from RFC 7519 Section 6.1 + let rfc_jwt = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."; + let decode_matcher = App::command() + .try_get_matches_from(vec!["jwt", "decode", "--ignore-exp", rfc_jwt]) + .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(), "Should decode unsecured JWT from RFC 7519: {:?}", result.err()); + let token = result.unwrap(); + assert_eq!( + token.claims.0.get("iss").and_then(|v| v.as_str()), + Some("joe") + ); + } + + #[test] + fn roundtrips_unsecured_jwt() { + let encode_matcher = App::command() + .try_get_matches_from(vec![ + "jwt", + "encode", + "-A", + "none", + "--no-iat", + "-P", + "test=roundtrip", + ]) + .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", &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(), "Should roundtrip unsecured JWT: {:?}", result.err()); + let token = result.unwrap(); + assert_eq!( + token.claims.0.get("test").and_then(|v| v.as_str()), + Some("roundtrip") + ); + } }