Skip to content

Commit

Permalink
support JWKS secret
Browse files Browse the repository at this point in the history
  • Loading branch information
deepu105 committed Jan 12, 2024
1 parent ed53a8d commit bd5fbc4
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 70 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased - 2023-XX-YY

## [1.0.1] - 2024-01-12

- Disable audience validation
- Support JWKS secret from argument and file
- Update libraries

## [1.0.0] - 2024-01-11

- Initial release
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jwt-ui"
version = "1.0.0"
version = "1.0.1"
authors = ["Deepu K Sasidharan <[email protected]>"]
description = """
A Terminal UI for decoding/encoding JSON Web Tokens
Expand Down
215 changes: 172 additions & 43 deletions src/app/jwt_decoder.rs

Large diffs are not rendered by default.

36 changes: 23 additions & 13 deletions src/app/jwt_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use jsonwebtoken::{errors::Error, Algorithm, EncodingKey, Header};

use super::{
jwt_decoder::Payload,
jwt_utils::{get_secret, JWTResult},
jwt_utils::{get_secret, JWTError, JWTResult, SecretFileType},
models::{ScrollableTxt, TabRoute, TabsState},
ActiveBlock, App, Route, RouteId, TextAreaInput, TextInput,
};
Expand Down Expand Up @@ -108,7 +108,8 @@ pub fn encode_jwt_token(app: &mut App) {
}

pub fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult<EncodingKey> {
let secret = get_secret(alg, secret_string)?;
let (secret, file_type) = get_secret(alg, secret_string);
let secret = secret?;

match alg {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => Ok(EncodingKey::from_secret(&secret)),
Expand All @@ -117,17 +118,26 @@ pub fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResu
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512 => match secret_string.ends_with(".pem") {
true => EncodingKey::from_rsa_pem(&secret).map_err(Error::into),
false => Ok(EncodingKey::from_rsa_der(&secret)),
| Algorithm::PS512 => match file_type {
SecretFileType::Pem => EncodingKey::from_rsa_pem(&secret).map_err(Error::into),
SecretFileType::Der => Ok(EncodingKey::from_rsa_der(&secret)),
_ => Err(JWTError::Internal(format!(
"Invalid secret file type for {alg:?}"
))),
},
Algorithm::ES256 | Algorithm::ES384 => match secret_string.ends_with(".pem") {
true => EncodingKey::from_ec_pem(&secret).map_err(Error::into),
false => Ok(EncodingKey::from_ec_der(&secret)),
Algorithm::ES256 | Algorithm::ES384 => match file_type {
SecretFileType::Pem => EncodingKey::from_ec_pem(&secret).map_err(Error::into),
SecretFileType::Der => Ok(EncodingKey::from_ec_der(&secret)),
_ => Err(JWTError::Internal(format!(
"Invalid secret file type for {alg:?}"
))),
},
Algorithm::EdDSA => match secret_string.ends_with(".pem") {
true => EncodingKey::from_ed_pem(&secret).map_err(Error::into),
false => Ok(EncodingKey::from_ed_der(&secret)),
Algorithm::EdDSA => match file_type {
SecretFileType::Pem => EncodingKey::from_ed_pem(&secret).map_err(Error::into),
SecretFileType::Der => Ok(EncodingKey::from_ed_der(&secret)),
_ => Err(JWTError::Internal(format!(
"Invalid secret file type for {alg:?}"
))),
},
}
}
Expand Down Expand Up @@ -179,7 +189,7 @@ mod tests {
];
app.data.encoder.payload.input = claims.clone().into();

app.data.encoder.secret.input = "@./test_data/test_rsa_private.pem".into();
app.data.encoder.secret.input = "@./test_data/test_rsa_private_key.pem".into();

encode_jwt_token(&mut app);
assert_eq!(app.data.error, "");
Expand All @@ -198,7 +208,7 @@ mod tests {
.retain(|claim| claim != "exp");
secret_validator.validate_exp = false;

let secret_string = "@./test_data/test_rsa_public.pem";
let secret_string = "@./test_data/test_rsa_public_key.pem";

let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>()).unwrap();

Expand Down
116 changes: 104 additions & 12 deletions src/app/jwt_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine as _};
use jsonwebtoken::{
errors::{Error, ErrorKind},
Algorithm,
jwk, Algorithm, DecodingKey, Header,
};

use super::utils::slurp_file;
Expand Down Expand Up @@ -56,29 +56,121 @@ impl fmt::Display for JWTError {
}
}

pub fn get_secret(alg: &Algorithm, secret_string: &str) -> JWTResult<Vec<u8>> {
pub enum SecretFileType {
Pem,
Der,
Jwks,
Na,
}

pub fn get_secret(alg: &Algorithm, secret_string: &str) -> (JWTResult<Vec<u8>>, SecretFileType) {
return match alg {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
if secret_string.starts_with('@') {
slurp_file(&secret_string.chars().skip(1).collect::<String>()).map_err(JWTError::from)
(
slurp_file(&secret_string.chars().skip(1).collect::<String>()).map_err(JWTError::from),
SecretFileType::Na,
)
} else if secret_string.starts_with("b64:") {
base64_engine
.decode(secret_string.chars().skip(4).collect::<String>())
.map_err(JWTError::from)
(
base64_engine
.decode(secret_string.chars().skip(4).collect::<String>())
.map_err(JWTError::from),
SecretFileType::Na,
)
} else {
Ok(secret_string.as_bytes().to_owned())
(Ok(secret_string.as_bytes().to_owned()), SecretFileType::Na)
}
}
_ => {
Algorithm::EdDSA => {
if !&secret_string.starts_with('@') {
return Err(JWTError::Internal(format!(
"Secret for {alg:?} must be a file path starting with @",
)));
return (
Err(JWTError::Internal(format!(
"Secret for {alg:?} must be a file path starting with @",
))),
SecretFileType::Na,
);
}

(
slurp_file(&secret_string.chars().skip(1).collect::<String>()).map_err(JWTError::from),
get_secret_file_type(secret_string),
)
}
_ => {
if secret_string.starts_with('@') {
(
slurp_file(&secret_string.chars().skip(1).collect::<String>()).map_err(JWTError::from),
get_secret_file_type(secret_string),
)
} else {
// allows to read JWKS from argument (e.g. output of 'curl https://auth.domain.com/jwks.json')
(Ok(secret_string.as_bytes().to_vec()), SecretFileType::Jwks)
}
}
};
}

pub fn decoding_key_from_jwks_secret(
secret: &[u8],
header: Option<Header>,
) -> JWTResult<DecodingKey> {
if let Some(h) = header {
return match parse_jwks(secret) {
Some(jwks) => decoding_key_from_jwks(jwks, &h),
None => Err(JWTError::Internal("Invalid jwks secret format".to_string())),
};
}
Err(JWTError::Internal(
"Invalid jwt header for jwks secret".to_string(),
))
}

fn decoding_key_from_jwks(jwks: jwk::JwkSet, header: &Header) -> JWTResult<DecodingKey> {
let kid = match &header.kid {
Some(k) => k.to_owned(),
None => {
return Err(JWTError::Internal(
"Missing 'kid' from jwt header. Required for jwks secret".to_string(),
));
}
};

slurp_file(&secret_string.chars().skip(1).collect::<String>()).map_err(JWTError::from)
let jwk = match jwks.find(&kid) {
Some(j) => j,
None => {
return Err(JWTError::Internal(format!(
"No jwk found for 'kid' {kid:?}",
)));
}
};

match &jwk.algorithm {
jwk::AlgorithmParameters::RSA(rsa) => {
DecodingKey::from_rsa_components(&rsa.n, &rsa.e).map_err(Error::into)
}
jwk::AlgorithmParameters::EllipticCurve(ec) => {
DecodingKey::from_ec_components(&ec.x, &ec.y).map_err(Error::into)
}
_ => Err(JWTError::Internal("Unsupported alg".to_string())),
}
}

fn parse_jwks(secret: &[u8]) -> Option<jwk::JwkSet> {
match serde_json::from_slice(secret) {
Ok(jwks) => Some(jwks),
Err(_) => None,
}
}

fn get_secret_file_type(secret_string: &str) -> SecretFileType {
if secret_string.ends_with(".pem") {
SecretFileType::Pem
} else if secret_string.ends_with(".json") {
SecretFileType::Jwks
} else {
SecretFileType::Der
}
}

fn map_external_error(ext_err: &Error) -> String {
Expand Down
File renamed without changes.
20 changes: 20 additions & 0 deletions test_data/test_ecdsa_public_jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"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"
}
]
}
File renamed without changes.
3 changes: 3 additions & 0 deletions test_data/test_eddsa_private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIOlt2x5aBpWjO8MNAiE7h9nfpZqFDXVBoRAuZu85fWMU
-----END PRIVATE KEY-----
3 changes: 3 additions & 0 deletions test_data/test_eddsa_public_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEABzKf6VPTrsj8oqur2gHMkpRCl2DHxe04q0A8lV/QP+A=
-----END PUBLIC KEY-----
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions test_data/test_rsa_public_jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"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"
}
]
}
File renamed without changes.
File renamed without changes.

0 comments on commit bd5fbc4

Please sign in to comment.