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