1 use atty::Stream;
2 use base64::decode as base64_decode;
3 use chrono::{TimeZone, Utc};
4 use clap::{arg_enum, crate_authors, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand};
5 use jsonwebtoken::errors::{ErrorKind, Result as JWTResult};
6 use jsonwebtoken::{
7     dangerous_insecure_decode, decode, encode, Algorithm, DecodingKey, EncodingKey, Header,
8     TokenData, Validation,
9 };
10 use serde_derive::{Deserialize, Serialize};
11 use serde_json::{from_str, to_string_pretty, Value};
12 use std::collections::BTreeMap;
13 use std::process::exit;
14 use std::{fs, io};
15 
16 #[derive(Debug, Serialize, Deserialize, PartialEq)]
17 struct PayloadItem(String, Value);
18 
19 #[derive(Debug, Serialize, Deserialize, PartialEq)]
20 struct Payload(BTreeMap<String, Value>);
21 
22 #[derive(Debug, Serialize, Deserialize, PartialEq)]
23 struct TokenOutput {
24     header: Header,
25     payload: Payload,
26 }
27 
28 arg_enum! {
29     #[allow(clippy::clippy::upper_case_acronyms)]
30     #[derive(Debug, PartialEq)]
31     enum SupportedAlgorithms {
32         HS256,
33         HS384,
34         HS512,
35         RS256,
36         RS384,
37         RS512,
38         PS256,
39         PS384,
40         PS512,
41         ES256,
42         ES384,
43     }
44 }
45 
46 arg_enum! {
47     #[allow(clippy::clippy::upper_case_acronyms)]
48     enum SupportedTypes {
49         JWT
50     }
51 }
52 
53 #[derive(Debug, PartialEq)]
54 enum OutputFormat {
55     Text,
56     Json,
57 }
58 
59 impl PayloadItem {
from_string(val: Option<&str>) -> Option<PayloadItem>60     fn from_string(val: Option<&str>) -> Option<PayloadItem> {
61         val.map(|item| PayloadItem::split_payload_item(item))
62     }
63 
from_string_with_name(val: Option<&str>, name: &str) -> Option<PayloadItem>64     fn from_string_with_name(val: Option<&str>, name: &str) -> Option<PayloadItem> {
65         match val {
66             Some(value) => match from_str(value) {
67                 Ok(json_value) => Some(PayloadItem(name.to_string(), json_value)),
68                 Err(_) => match from_str(format!("\"{}\"", value).as_str()) {
69                     Ok(json_value) => Some(PayloadItem(name.to_string(), json_value)),
70                     Err(_) => None,
71                 },
72             },
73             _ => None,
74         }
75     }
76 
77     // If the value is defined as systemd.time, converts the defined duration into a UNIX timestamp
from_timestamp_with_name(val: Option<&str>, name: &str, now: i64) -> Option<PayloadItem>78     fn from_timestamp_with_name(val: Option<&str>, name: &str, now: i64) -> Option<PayloadItem> {
79         if let Some(timestamp) = val {
80             if timestamp.parse::<u64>().is_err() {
81                 let duration = parse_duration::parse(timestamp);
82                 if let Ok(parsed_duration) = duration {
83                     let seconds = parsed_duration.as_secs() + now as u64;
84                     return PayloadItem::from_string_with_name(Some(&seconds.to_string()), name);
85                 }
86             }
87         }
88 
89         PayloadItem::from_string_with_name(val, name)
90     }
91 
split_payload_item(p: &str) -> PayloadItem92     fn split_payload_item(p: &str) -> PayloadItem {
93         let split: Vec<&str> = p.split('=').collect();
94         let (name, value) = (split[0], split[1]);
95         let payload_item = PayloadItem::from_string_with_name(Some(value), name);
96 
97         payload_item.unwrap()
98     }
99 }
100 
101 impl Payload {
from_payloads(payloads: Vec<PayloadItem>) -> Payload102     fn from_payloads(payloads: Vec<PayloadItem>) -> Payload {
103         let mut payload = BTreeMap::new();
104 
105         for PayloadItem(k, v) in payloads {
106             payload.insert(k, v);
107         }
108 
109         Payload(payload)
110     }
111 
convert_timestamps(&mut self)112     fn convert_timestamps(&mut self) {
113         let timestamp_claims: Vec<String> = vec!["iat".into(), "nbf".into(), "exp".into()];
114 
115         for (key, value) in self.0.iter_mut() {
116             if timestamp_claims.contains(key) && value.is_number() {
117                 *value = match value.as_i64() {
118                     Some(timestamp) => Utc.timestamp(timestamp, 0).to_rfc3339().into(),
119                     None => value.clone(),
120                 }
121             }
122         }
123     }
124 }
125 
126 impl SupportedAlgorithms {
from_string(alg: &str) -> SupportedAlgorithms127     fn from_string(alg: &str) -> SupportedAlgorithms {
128         match alg {
129             "HS256" => SupportedAlgorithms::HS256,
130             "HS384" => SupportedAlgorithms::HS384,
131             "HS512" => SupportedAlgorithms::HS512,
132             "RS256" => SupportedAlgorithms::RS256,
133             "RS384" => SupportedAlgorithms::RS384,
134             "RS512" => SupportedAlgorithms::RS512,
135             "PS256" => SupportedAlgorithms::PS256,
136             "PS384" => SupportedAlgorithms::PS384,
137             "PS512" => SupportedAlgorithms::PS512,
138             "ES256" => SupportedAlgorithms::ES256,
139             "ES384" => SupportedAlgorithms::ES384,
140             _ => SupportedAlgorithms::HS256,
141         }
142     }
143 }
144 
145 impl TokenOutput {
new(data: TokenData<Payload>) -> Self146     fn new(data: TokenData<Payload>) -> Self {
147         TokenOutput {
148             header: data.header,
149             payload: data.claims,
150         }
151     }
152 }
153 
config_options<'a, 'b>() -> App<'a, 'b>154 fn config_options<'a, 'b>() -> App<'a, 'b> {
155     App::new("jwt")
156         .about("Encode and decode JWTs from the command line. RSA and ECDSA encryption currently only supports keys in DER format")
157         .version(crate_version!())
158         .author(crate_authors!())
159         .setting(AppSettings::ArgRequiredElseHelp)
160         .subcommand(
161             SubCommand::with_name("encode")
162                 .about("Encode new JWTs")
163                 .arg(
164                     Arg::with_name("algorithm")
165                         .help("the algorithm to use for signing the JWT")
166                         .takes_value(true)
167                         .long("alg")
168                         .short("A")
169                         .possible_values(&SupportedAlgorithms::variants())
170                         .default_value("HS256"),
171                 ).arg(
172                     Arg::with_name("kid")
173                         .help("the kid to place in the header")
174                         .takes_value(true)
175                         .long("kid")
176                         .short("k"),
177                 ).arg(
178                     Arg::with_name("type")
179                         .help("the type of token being encoded")
180                         .takes_value(true)
181                         .long("typ")
182                         .short("t")
183                         .possible_values(&SupportedTypes::variants()),
184                 ).arg(
185                     Arg::with_name("json")
186                         .help("the json payload to encode")
187                         .index(1)
188                         .required(false),
189                 ).arg(
190                     Arg::with_name("payload")
191                         .help("a key=value pair to add to the payload")
192                         .number_of_values(1)
193                         .multiple(true)
194                         .takes_value(true)
195                         .long("payload")
196                         .short("P")
197                         .validator(is_payload_item),
198                 ).arg(
199                     Arg::with_name("expires")
200                         .help("the time the token should expire, in seconds or systemd.time string")
201                         .default_value("+30 min")
202                         .takes_value(true)
203                         .long("exp")
204                         .short("e")
205                         .validator(is_timestamp_or_duration),
206                 ).arg(
207                     Arg::with_name("issuer")
208                         .help("the issuer of the token")
209                         .takes_value(true)
210                         .long("iss")
211                         .short("i"),
212                 ).arg(
213                     Arg::with_name("subject")
214                         .help("the subject of the token")
215                         .takes_value(true)
216                         .long("sub")
217                         .short("s"),
218                 ).arg(
219                     Arg::with_name("audience")
220                         .help("the audience of the token")
221                         .takes_value(true)
222                         .long("aud")
223                         .short("a")
224                 ).arg(
225                     Arg::with_name("jwt_id")
226                         .help("the jwt id of the token")
227                         .takes_value(true)
228                         .long("jti")
229                 ).arg(
230                     Arg::with_name("not_before")
231                         .help("the time the JWT should become valid, in seconds or systemd.time string")
232                         .takes_value(true)
233                         .long("nbf")
234                         .short("n")
235                         .validator(is_timestamp_or_duration),
236                 ).arg(
237                     Arg::with_name("no_iat")
238                         .help("prevent an iat claim from being automatically added")
239                         .long("no-iat")
240                 ).arg(
241                     Arg::with_name("secret")
242                         .help("the secret to sign the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes")
243                         .takes_value(true)
244                         .long("secret")
245                         .short("S")
246                         .required(true),
247                 ),
248         ).subcommand(
249             SubCommand::with_name("decode")
250                 .about("Decode a JWT")
251                 .arg(
252                     Arg::with_name("jwt")
253                         .help("the jwt to decode")
254                         .index(1)
255                         .required(true),
256                 ).arg(
257                     Arg::with_name("algorithm")
258                         .help("the algorithm to use for signing the JWT")
259                         .takes_value(true)
260                         .long("alg")
261                         .short("A")
262                         .possible_values(&SupportedAlgorithms::variants())
263                         .default_value("HS256"),
264                 ).arg(
265                     Arg::with_name("iso_dates")
266                         .help("display unix timestamps as ISO 8601 dates")
267                         .takes_value(false)
268                         .long("iso8601")
269                 ).arg(
270                     Arg::with_name("secret")
271                         .help("the secret to validate the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes")
272                         .takes_value(true)
273                         .long("secret")
274                         .short("S")
275                         .default_value(""),
276                 ).arg(
277                     Arg::with_name("json")
278                         .help("render decoded JWT as JSON")
279                         .long("json")
280                         .short("j"),
281                 ).arg(
282                     Arg::with_name("ignore_exp")
283                         .help("Ignore token expiration date (`exp` claim) during validation.")
284                         .long("ignore-exp")
285                 ),
286         )
287 }
288 
is_timestamp_or_duration(val: String) -> Result<(), String>289 fn is_timestamp_or_duration(val: String) -> Result<(), String> {
290     match val.parse::<i64>() {
291         Ok(_) => Ok(()),
292         Err(_) => match parse_duration::parse(&val) {
293             Ok(_) => Ok(()),
294             Err(_) => Err(String::from(
295                 "must be a UNIX timestamp or systemd.time string",
296             )),
297         },
298     }
299 }
300 
is_payload_item(val: String) -> Result<(), String>301 fn is_payload_item(val: String) -> Result<(), String> {
302     match val.split('=').count() {
303         2 => Ok(()),
304         _ => Err(String::from(
305             "payloads must have a key and value in the form key=value",
306         )),
307     }
308 }
309 
warn_unsupported(matches: &ArgMatches)310 fn warn_unsupported(matches: &ArgMatches) {
311     if matches.value_of("type").is_some() {
312         println!("Sorry, `typ` isn't supported quite yet!");
313     }
314 }
315 
translate_algorithm(alg: SupportedAlgorithms) -> Algorithm316 fn translate_algorithm(alg: SupportedAlgorithms) -> Algorithm {
317     match alg {
318         SupportedAlgorithms::HS256 => Algorithm::HS256,
319         SupportedAlgorithms::HS384 => Algorithm::HS384,
320         SupportedAlgorithms::HS512 => Algorithm::HS512,
321         SupportedAlgorithms::RS256 => Algorithm::RS256,
322         SupportedAlgorithms::RS384 => Algorithm::RS384,
323         SupportedAlgorithms::RS512 => Algorithm::RS512,
324         SupportedAlgorithms::PS256 => Algorithm::PS256,
325         SupportedAlgorithms::PS384 => Algorithm::PS384,
326         SupportedAlgorithms::PS512 => Algorithm::PS512,
327         SupportedAlgorithms::ES256 => Algorithm::ES256,
328         SupportedAlgorithms::ES384 => Algorithm::ES384,
329     }
330 }
331 
create_header(alg: Algorithm, kid: Option<&str>) -> Header332 fn create_header(alg: Algorithm, kid: Option<&str>) -> Header {
333     let mut header = Header::new(alg);
334 
335     header.kid = kid.map(str::to_string);
336 
337     header
338 }
339 
slurp_file(file_name: &str) -> Vec<u8>340 fn slurp_file(file_name: &str) -> Vec<u8> {
341     fs::read(file_name).unwrap_or_else(|_| panic!("Unable to read file {}", file_name))
342 }
343 
encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult<EncodingKey>344 fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult<EncodingKey> {
345     match alg {
346         Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
347             if secret_string.starts_with('@') {
348                 let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
349                 Ok(EncodingKey::from_secret(&secret))
350             } else if secret_string.starts_with("b64:") {
351                 Ok(EncodingKey::from_secret(
352                     &base64_decode(&secret_string.chars().skip(4).collect::<String>()).unwrap(),
353                 ))
354             } else {
355                 Ok(EncodingKey::from_secret(secret_string.as_bytes()))
356             }
357         }
358         Algorithm::RS256
359         | Algorithm::RS384
360         | Algorithm::RS512
361         | Algorithm::PS256
362         | Algorithm::PS384
363         | Algorithm::PS512 => {
364             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
365 
366             match secret_string.ends_with(".pem") {
367                 true => EncodingKey::from_rsa_pem(&secret),
368                 false => Ok(EncodingKey::from_rsa_der(&secret)),
369             }
370         }
371         Algorithm::ES256 | Algorithm::ES384 => {
372             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
373 
374             match secret_string.ends_with(".pem") {
375                 true => EncodingKey::from_ec_pem(&secret),
376                 false => Ok(EncodingKey::from_ec_der(&secret)),
377             }
378         }
379     }
380 }
381 
decoding_key_from_secret( alg: &Algorithm, secret_string: &str, ) -> JWTResult<DecodingKey<'static>>382 fn decoding_key_from_secret(
383     alg: &Algorithm,
384     secret_string: &str,
385 ) -> JWTResult<DecodingKey<'static>> {
386     match alg {
387         Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
388             if secret_string.starts_with('@') {
389                 let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
390                 Ok(DecodingKey::from_secret(&secret).into_static())
391             } else if secret_string.starts_with("b64:") {
392                 Ok(DecodingKey::from_secret(
393                     &base64_decode(&secret_string.chars().skip(4).collect::<String>()).unwrap(),
394                 )
395                 .into_static())
396             } else {
397                 Ok(DecodingKey::from_secret(secret_string.as_bytes()).into_static())
398             }
399         }
400         Algorithm::RS256
401         | Algorithm::RS384
402         | Algorithm::RS512
403         | Algorithm::PS256
404         | Algorithm::PS384
405         | Algorithm::PS512 => {
406             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
407 
408             match secret_string.ends_with(".pem") {
409                 true => DecodingKey::from_rsa_pem(&secret).map(DecodingKey::into_static),
410                 false => Ok(DecodingKey::from_rsa_der(&secret).into_static()),
411             }
412         }
413         Algorithm::ES256 | Algorithm::ES384 => {
414             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
415 
416             match secret_string.ends_with(".pem") {
417                 true => DecodingKey::from_ec_pem(&secret).map(DecodingKey::into_static),
418                 false => Ok(DecodingKey::from_ec_der(&secret).into_static()),
419             }
420         }
421     }
422 }
423 
encode_token(matches: &ArgMatches) -> JWTResult<String>424 fn encode_token(matches: &ArgMatches) -> JWTResult<String> {
425     let algorithm = translate_algorithm(SupportedAlgorithms::from_string(
426         matches.value_of("algorithm").unwrap(),
427     ));
428     let kid = matches.value_of("kid");
429     let header = create_header(algorithm, kid);
430     let custom_payloads: Option<Vec<Option<PayloadItem>>> =
431         matches.values_of("payload").map(|maybe_payloads| {
432             maybe_payloads
433                 .map(|p| PayloadItem::from_string(Some(p)))
434                 .collect()
435         });
436     let custom_payload = matches
437         .value_of("json")
438         .map(|value| {
439             if value != "-" {
440                 return String::from(value);
441             }
442 
443             let mut buffer = String::new();
444 
445             io::stdin()
446                 .read_line(&mut buffer)
447                 .expect("STDIN was not valid UTF-8");
448 
449             buffer
450         })
451         .map(|raw_json| match from_str(&raw_json) {
452             Ok(Value::Object(json_value)) => json_value
453                 .into_iter()
454                 .map(|(json_key, json_val)| Some(PayloadItem(json_key, json_val)))
455                 .collect(),
456             _ => panic!("Invalid JSON provided!"),
457         });
458     let now = Utc::now().timestamp();
459     let expires = match matches.occurrences_of("expires") {
460         0 => None,
461         _ => PayloadItem::from_timestamp_with_name(matches.value_of("expires"), "exp", now),
462     };
463     let not_before =
464         PayloadItem::from_timestamp_with_name(matches.value_of("not_before"), "nbf", now);
465     let issued_at = match matches.is_present("no_iat") {
466         true => None,
467         false => PayloadItem::from_timestamp_with_name(Some(&now.to_string()), "iat", now),
468     };
469     let issuer = PayloadItem::from_string_with_name(matches.value_of("issuer"), "iss");
470     let subject = PayloadItem::from_string_with_name(matches.value_of("subject"), "sub");
471     let audience = PayloadItem::from_string_with_name(matches.value_of("audience"), "aud");
472     let jwt_id = PayloadItem::from_string_with_name(matches.value_of("jwt_id"), "jti");
473     let mut maybe_payloads: Vec<Option<PayloadItem>> = vec![
474         issued_at, expires, issuer, subject, audience, jwt_id, not_before,
475     ];
476 
477     maybe_payloads.append(&mut custom_payloads.unwrap_or_default());
478     maybe_payloads.append(&mut custom_payload.unwrap_or_default());
479 
480     let payloads = maybe_payloads.into_iter().flatten().collect();
481     let Payload(claims) = Payload::from_payloads(payloads);
482 
483     encoding_key_from_secret(&algorithm, matches.value_of("secret").unwrap())
484         .and_then(|secret| encode(&header, &claims, &secret))
485 }
486 
decode_token( matches: &ArgMatches, ) -> ( JWTResult<TokenData<Payload>>, JWTResult<TokenData<Payload>>, OutputFormat, )487 fn decode_token(
488     matches: &ArgMatches,
489 ) -> (
490     JWTResult<TokenData<Payload>>,
491     JWTResult<TokenData<Payload>>,
492     OutputFormat,
493 ) {
494     let algorithm = translate_algorithm(SupportedAlgorithms::from_string(
495         matches.value_of("algorithm").unwrap(),
496     ));
497     let secret = match matches.value_of("secret").map(|s| (s, !s.is_empty())) {
498         Some((secret, true)) => Some(decoding_key_from_secret(&algorithm, secret)),
499         _ => None,
500     };
501     let jwt = matches
502         .value_of("jwt")
503         .map(|value| {
504             if value != "-" {
505                 return String::from(value);
506             }
507 
508             let mut buffer = String::new();
509 
510             io::stdin()
511                 .read_line(&mut buffer)
512                 .expect("STDIN was not valid UTF-8");
513 
514             buffer
515         })
516         .unwrap()
517         .trim()
518         .to_owned();
519 
520     let secret_validator = Validation {
521         leeway: 1000,
522         algorithms: vec![algorithm],
523         validate_exp: !matches.is_present("ignore_exp"),
524         ..Default::default()
525     };
526 
527     let token_data = dangerous_insecure_decode::<Payload>(&jwt).map(|mut token| {
528         if matches.is_present("iso_dates") {
529             token.claims.convert_timestamps();
530         }
531 
532         token
533     });
534 
535     (
536         match secret {
537             Some(secret_key) => decode::<Payload>(&jwt, &secret_key.unwrap(), &secret_validator),
538             None => dangerous_insecure_decode::<Payload>(&jwt),
539         },
540         token_data,
541         if matches.is_present("json") {
542             OutputFormat::Json
543         } else {
544             OutputFormat::Text
545         },
546     )
547 }
548 
print_encoded_token(token: JWTResult<String>)549 fn print_encoded_token(token: JWTResult<String>) {
550     match token {
551         Ok(jwt) => {
552             if atty::is(Stream::Stdout) {
553                 println!("{}", jwt);
554             } else {
555                 print!("{}", jwt);
556             }
557             exit(0);
558         }
559         Err(err) => {
560             bunt::eprintln!("{$red+bold}Something went awry creating the jwt{/$}\n");
561             eprintln!("{}", err);
562             exit(1);
563         }
564     }
565 }
566 
print_decoded_token( validated_token: JWTResult<TokenData<Payload>>, token_data: JWTResult<TokenData<Payload>>, format: OutputFormat, )567 fn print_decoded_token(
568     validated_token: JWTResult<TokenData<Payload>>,
569     token_data: JWTResult<TokenData<Payload>>,
570     format: OutputFormat,
571 ) {
572     if let Err(err) = &validated_token {
573         match err.kind() {
574             ErrorKind::InvalidToken => {
575                 bunt::println!("{$red+bold}The JWT provided is invalid{/$}")
576             }
577             ErrorKind::InvalidSignature => {
578                 bunt::eprintln!("{$red+bold}The JWT provided has an invalid signature{/$}")
579             }
580             ErrorKind::InvalidRsaKey => {
581                 bunt::eprintln!("{$red+bold}The secret provided isn't a valid RSA key{/$}")
582             }
583             ErrorKind::InvalidEcdsaKey => {
584                 bunt::eprintln!("{$red+bold}The secret provided isn't a valid ECDSA key{/$}")
585             }
586             ErrorKind::ExpiredSignature => {
587                 bunt::eprintln!("{$red+bold}The token has expired (or the `exp` claim is not set). This error can be ignored via the `--ignore-exp` parameter.{/$}")
588             }
589             ErrorKind::InvalidIssuer => {
590                 bunt::println!("{$red+bold}The token issuer is invalid{/$}")
591             }
592             ErrorKind::InvalidAudience => {
593                 bunt::eprintln!("{$red+bold}The token audience doesn't match the subject{/$}")
594             }
595             ErrorKind::InvalidSubject => {
596                 bunt::eprintln!("{$red+bold}The token subject doesn't match the audience{/$}")
597             }
598             ErrorKind::ImmatureSignature => bunt::eprintln!(
599                 "{$red+bold}The `nbf` claim is in the future which isn't allowed{/$}"
600             ),
601             ErrorKind::InvalidAlgorithm => bunt::eprintln!(
602                 "{$red+bold}The JWT provided has a different signing algorithm than the one you \
603                      provided{/$}",
604             ),
605             _ => bunt::eprintln!(
606                 "{$red+bold}The JWT provided is invalid because{/$} {:?}",
607                 err
608             ),
609         };
610     }
611 
612     match (format, token_data) {
613         (OutputFormat::Json, Ok(token)) => {
614             println!("{}", to_string_pretty(&TokenOutput::new(token)).unwrap())
615         }
616         (_, Ok(token)) => {
617             bunt::println!("\n{$bold}Token header\n------------{/$}");
618             println!("{}\n", to_string_pretty(&token.header).unwrap());
619             bunt::println!("{$bold}Token claims\n------------{/$}");
620             println!("{}", to_string_pretty(&token.claims).unwrap());
621         }
622         (_, Err(_)) => exit(1),
623     }
624 
625     exit(match validated_token {
626         Err(_) => 1,
627         Ok(_) => 0,
628     })
629 }
630 
main()631 fn main() {
632     let matches = config_options().get_matches();
633 
634     match matches.subcommand() {
635         ("encode", Some(encode_matches)) => {
636             warn_unsupported(encode_matches);
637 
638             let token = encode_token(encode_matches);
639 
640             print_encoded_token(token);
641         }
642         ("decode", Some(decode_matches)) => {
643             let (validated_token, token_data, format) = decode_token(decode_matches);
644 
645             print_decoded_token(validated_token, token_data, format);
646         }
647         _ => (),
648     }
649 }
650