Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use a jwk for decoding a jwt? #96

Closed
axos88 opened this issue Jun 21, 2018 · 11 comments
Closed

How to use a jwk for decoding a jwt? #96

axos88 opened this issue Jun 21, 2018 · 11 comments
Labels

Comments

@axos88
Copy link
Contributor

axos88 commented Jun 21, 2018

Hello,

Can you please provide an example on how to use a jwk to decode an RS256 jwt?

What I got so far:

    let jwks: JWKSet<Empty> = serde_json::from_str(include_str!("jwks.json"))?;
    let jwk  = jwks.find("42").unwrap();
    let jwt: JWT<Empty, Empty> = JWT::new_encoded(include_str!("token.jwt"));

    let key_parameters = if let AlgorithmParameters::RSA(ref kp) = jwk.algorithm {
        Some(kp)
    } else { None }.unwrap();


    let secret = Secret::PublicKey(??????);
    let algorithm = SignatureAlgorithm::RS256;

    let decoded = jwt.decode(&secret, algorithm);

    println!("jwk = {:?} \n\n\n jwt = {:?}\n\n\n decoded= {:?}", jwk, jwt, decoded))
@lawliet89
Copy link
Owner

Sorry I haven't touched this library for a while.

There is currently no code to construct a key usable by ring from a JWK because there is no such functionality in ring. You might have to look into something like openssl at the moment.

@axos88
Copy link
Contributor Author

axos88 commented Jun 21, 2018

I tried that, but openssl seems to have a different type for BigNums, and it seems there is no way to convert between the two :|

@lawliet89
Copy link
Owner

I see. A thought: is there any way to convert the BigNums into little/big endian byte arrays and then have the other library reconstruct it from the byte array with the appropriate endianness?

@axos88
Copy link
Contributor Author

axos88 commented Jun 21, 2018

Yeah, it seems to support pub fn BigNum::from_slice(n: &[u8]), which expects a big endian unsigned array

@axos88
Copy link
Contributor Author

axos88 commented Jun 21, 2018

I see where you're going, but it doesn't seem to be working:

    let jwks: JWKSet<Empty> = serde_json::from_str(include_str!("jwks.json"))?;

    let jwk  = jwks.find("42").unwrap();

    let jwt: JWT<Empty, Empty> = JWT::new_encoded(include_str!("access.jwt"));

    let key_parameters = if let AlgorithmParameters::RSA(ref kp) = jwk.algorithm {
        Some(kp)
    } else { None }.unwrap();

    let n_be = key_parameters.n.to_bytes_be();
    let e_be = key_parameters.e.to_bytes_be();

    let n_bn = openssl::bn::BigNum::from_slice(&n_be).unwrap();
    let e_bn = openssl::bn::BigNum::from_slice(&e_be).unwrap();

    let pubkey = openssl::rsa::Rsa::from_public_components(n_bn, e_bn).unwrap();

    let der = pubkey.public_key_to_der().unwrap();

    let secret = Secret::PublicKey(der);
    let algorithm = SignatureAlgorithm::RS256;

    let decoded = jwt.decode(&secret, algorithm);

    Ok(format!("jwk = {:?} \n\n\n jwt = {:?}\n\n\n decoded= {:?}", jwk, jwt, decoded))

This returns Err(ValidationError(InvalidSignature)) for decoded

@axos88
Copy link
Contributor Author

axos88 commented Jun 21, 2018

Turns out should've used public_key_to_der_pkcs() instead...

The code below works:

    let jwks: JWKSet<Empty> = serde_json::from_str(include_str!("jwks.json"))?;
    let jwk  = jwks.find("42").unwrap();

    let key_parameters = if let AlgorithmParameters::RSA(ref kp) = jwk.algorithm {
        Some(kp)
    } else { None }.unwrap();
    let n_be = key_parameters.n.to_bytes_be();
    let e_be = key_parameters.e.to_bytes_be();
    let n_bn = openssl::bn::BigNum::from_slice(&n_be).unwrap();
    let e_bn = openssl::bn::BigNum::from_slice(&e_be).unwrap();
    let pubkey = openssl::rsa::Rsa::from_public_components(n_bn, e_bn).unwrap();
    let der: Vec<u8> = pubkey.public_key_to_der_pkcs1().unwrap();
    let secret = Secret::PublicKey(der);
    let algorithm = SignatureAlgorithm::RS256;

    let jwt: JWT<MyClaims, Empty> = JWT::new_encoded(include_str!("access.jwt"));
    let decoded = jwt.decode(&secret, algorithm);

    println!("decoded= {:?}", decoded));

@lawliet89
Copy link
Owner

Thanks for figuring this out. I might add this in the future to the documentation or add some optional functions to do this (behind a feature gate probably).

@axos88
Copy link
Contributor Author

axos88 commented Jun 29, 2018

Well this is mostly a hack... I hope your implementation will not go back-and-forth to openssl to do it, but i'm looking forward to it - it seems like a logical thing to do - use a jwk to get the public key to verify the signature of the jwt :)

@bbangert
Copy link

Ok, so, its actually a bit easier than above. I ended up having to use the fork here:
https://github.com/Korvox/biscuit

I loaded the JWK's from Amazon Cognito in this case, code to extract the custom claims is pretty short:

use biscuit::{
    jwa::SignatureAlgorithm,
    jwk::{AlgorithmParameters, JWKSet},
    jws::{Header, Secret},
    Empty, JWT,
};
use reqwest;

#[derive(Debug)]
struct AuthError {}

pub struct CognitoAuth {
    key_set: JWKSet<Empty>,
}

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct CognitoClaims {
    #[serde(rename = "cognito:username")]
    pub username: String,
    #[serde(rename = "custom:sub-exp")]
    pub sub_exp: String,
}

impl CognitoAuth {
    /// Create a new CognitoAuth object
    ///
    /// Blocking call that will panic if unable to load the initial JWK's from AWS.
    pub fn new(region: &str, user_pool_id: &str) -> Self {
        let url = format!(
            "https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json",
            region, user_pool_id
        );
        let data: JWKSet<Empty> = reqwest::get(&url)
            .expect("Unable to fetch JWK's")
            .json()
            .expect("Unable to load JWK response into JSON");
        CognitoAuth { key_set: data }
    }

    /// Extract an auth token and return a Cognito User object
    pub fn extract_auth_header(&self, token: &str) -> Result<CognitoClaims, AuthError> {
        // First extract without verifying the header to locate the key-id (kid)
        let token = JWT::<CognitoClaims, Empty>::new_encoded(token);
        let header: Header<Empty> = token.unverified_header().map_err(|_| AuthError {})?;
        let key_id = header.registered.key_id.ok_or(AuthError {})?;
        let key = self.key_set.find(&key_id).ok_or(AuthError {})?;

        // Now that we have the key, construct our RSA public key secret
        let secret = match key.algorithm {
            AlgorithmParameters::RSA(ref rsa_key) => Secret::Pkcs {
                n: rsa_key.n.clone(),
                e: rsa_key.e.clone(),
            },
            _ => return Err(AuthError {}),
        };

        // Not fully verify and extract the token with verification
        let token = token
            .into_decoded(&secret, SignatureAlgorithm::RS256)
            .map_err(|_| AuthError {})?;
        let user = token.header().map_err(|_| AuthError {})?;
        let payload = token.payload().map_err(|_| AuthError {})?;
        Ok(payload.private.clone())
    }
}

It'd be great to see the branch PR merged in and some examples polished up since I'd imagine using a JWK like this is a fairly common desire.

@lawliet89
Copy link
Owner

I would like to take what was implemented in #91 and tweak the design a bit. Give me a bit of time and I'll open a PR with that.

@lawliet89
Copy link
Owner

I've merged #100 with the code from #91. Also added a convenience method to construct the Secret from a RSAKeyParameters and a From impl.

I've released this as v0.1.0.

Not fully satisfied with how this is done. Ideally I would like to be rid of Secret and make everything go through a JWK instead, but I guess that's for later (#53).

Closing this for now. If you think this is unresolved, please reopen or raise a new issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants