ts-token 0.8.0

JSON web token library for my projects
Documentation
//! An issuer for a token.

use core::time::Duration;

use base64ct::{Base64UrlUnpadded, Encoding};
use rand::RngCore;
use ts_crypto::{
    Sha256, Sha384, Sha512, SigningKey,
    rsa::{Pkcs1v15, Pss},
};

use crate::{
    JsonWebKey,
    jwt::{Claims, Header},
};

/// An issuer for a token.
pub struct TokenIssuer {
    /// The URL a recipiant of the token can use to fetch the JSON web key.
    pub jku: String,
    /// The URL of this issuer.
    pub iss: String,
    /// The key for certain token parameters.
    pub jwk: JsonWebKey,
    /// The signing key.
    pub key: SigningKey,
}

impl TokenIssuer {
    /// Create a new issuer from its parts
    pub fn new(key: SigningKey, jwk: JsonWebKey, iss: String, jku: String) -> Self {
        Self { jku, iss, jwk, key }
    }

    /// Issue a signed JSON web token.
    ///
    /// ## Panics
    /// * If system time is before `UNIX_EPOCH`.
    /// * Serializing as JSON fails.
    /// * The signing key does not match the JSON web key algorithm.
    pub fn issue(&self, aud: String, sub: String, scopes: String, valid_for: Duration) -> String {
        let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
            panic!("system time is before UNIX_EPOCH")
        };
        let exp = (now + valid_for).as_secs();
        let iat = now.as_secs();

        let mut jti = [0; 32];
        rand::rng().fill_bytes(&mut jti[..]);
        let jti = Base64UrlUnpadded::encode_string(&jti);

        let header = Header {
            alg: self.jwk.alg.clone(),
            typ: "JWT".to_string(),
            kid: self.jwk.kid.clone(),
            jku: self.jku.clone(),
        };

        let claims = Claims {
            jti,
            iss: self.iss.clone(),
            exp,
            iat,
            sub,
            aud,
            scopes,
        };

        let header = Base64UrlUnpadded::encode_string(
            &serde_json::to_vec(&header).expect("serializing token parts should not fail"),
        );
        let claims = Base64UrlUnpadded::encode_string(
            &serde_json::to_vec(&claims).expect("serializing token parts should not fail"),
        );
        let message = [header, claims].join(".");

        let signature = match self.jwk.alg.as_str() {
            "RS256" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pkcs1v15, Sha256>(message.as_bytes())
            }
            "RS384" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pkcs1v15, Sha384>(message.as_bytes())
            }
            "RS512" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pkcs1v15, Sha512>(message.as_bytes())
            }

            "PS256" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pss, Sha256>(message.as_bytes())
            }
            "PS384" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pss, Sha384>(message.as_bytes())
            }
            "PS512" => {
                let SigningKey::Rsa(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Pss, Sha512>(message.as_bytes())
            }

            "ES256" => {
                let SigningKey::Elliptic(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Sha256>(message.as_bytes())
            }
            "ES384" => {
                let SigningKey::Elliptic(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Sha384>(message.as_bytes())
            }
            "ES512" => {
                let SigningKey::Elliptic(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign::<Sha512>(message.as_bytes())
            }

            "Ed25519" | "Ed448" | "EdDSA" => {
                let SigningKey::Edwards(key) = &self.key else {
                    panic!("signing key type does not match the JWK algorithm");
                };
                key.sign(message.as_bytes())
            }

            other => unimplemented!("the algorithm `{other}` is not supported"),
        };
        let signature = Base64UrlUnpadded::encode_string(&signature);

        [message, signature].join(".")
    }
}