use crate::error::{Error, Result};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::Serialize;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
const AUDIENCE: &str = "appstoreconnect-v1";
const MAX_TOKEN_LIFETIME_SECS: u64 = 20 * 60;
#[derive(Debug, Clone, Serialize)]
struct Claims {
iss: String,
iat: u64,
exp: u64,
aud: String,
}
#[derive(Clone)]
pub struct ApiKey {
key_id: String,
issuer_id: String,
encoding_key: EncodingKey,
lifetime_secs: u64,
}
impl ApiKey {
pub fn from_p8_file(
key_id: impl Into<String>,
issuer_id: impl Into<String>,
p8_path: impl AsRef<Path>,
) -> Result<Self> {
let path = p8_path.as_ref();
let pem = std::fs::read_to_string(path).map_err(|source| Error::PrivateKeyRead {
path: path.display().to_string(),
source,
})?;
Self::from_p8_pem(key_id, issuer_id, &pem)
}
pub fn from_p8_pem(
key_id: impl Into<String>,
issuer_id: impl Into<String>,
pem: &str,
) -> Result<Self> {
let encoding_key = EncodingKey::from_ec_pem(pem.as_bytes())?;
Ok(Self {
key_id: key_id.into(),
issuer_id: issuer_id.into(),
encoding_key,
lifetime_secs: MAX_TOKEN_LIFETIME_SECS,
})
}
pub fn with_lifetime_secs(mut self, secs: u64) -> Self {
self.lifetime_secs = secs.min(MAX_TOKEN_LIFETIME_SECS);
self
}
pub fn key_id(&self) -> &str {
&self.key_id
}
pub fn lifetime_secs(&self) -> u64 {
self.lifetime_secs
}
pub fn token(&self) -> Result<String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default() .as_secs();
let claims = Claims {
iss: self.issuer_id.clone(),
iat: now,
exp: now + self.lifetime_secs,
aud: AUDIENCE.to_string(),
};
let mut header = Header::new(Algorithm::ES256);
header.kid = Some(self.key_id.clone());
Ok(encode(&header, &claims, &self.encoding_key)?)
}
}