development
rs 99 lines 3.1 KB
Raw
1 use crate::error::{Error, Result};
2 use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
3 use serde::Serialize;
4 use std::path::Path;
5 use std::time::{SystemTime, UNIX_EPOCH};
6
7 const AUDIENCE: &str = "appstoreconnect-v1";
8 // Apple rejects tokens whose `exp` is more than 20 minutes past `iat`. See
9 // https://developer.apple.com/documentation/appstoreconnectapi/generating-tokens-for-api-requests
10 const MAX_TOKEN_LIFETIME_SECS: u64 = 20 * 60;
11
12 #[derive(Debug, Clone, Serialize)]
13 struct Claims {
14 iss: String,
15 iat: u64,
16 exp: u64,
17 aud: String,
18 }
19
20 /// An App Store Connect API key: the (key ID, issuer ID, private key)
21 /// triple from App Store Connect → Users and Access → Integrations → App
22 /// Store Connect API. Signs the short-lived ES256 JWTs the API requires as
23 /// a bearer token on every request.
24 #[derive(Clone)]
25 pub struct ApiKey {
26 key_id: String,
27 issuer_id: String,
28 encoding_key: EncodingKey,
29 lifetime_secs: u64,
30 }
31
32 impl ApiKey {
33 /// Load a key from a PEM-encoded `.p8` private key file — the file App
34 /// Store Connect lets you download once, at key-creation time.
35 pub fn from_p8_file(
36 key_id: impl Into<String>,
37 issuer_id: impl Into<String>,
38 p8_path: impl AsRef<Path>,
39 ) -> Result<Self> {
40 let path = p8_path.as_ref();
41 let pem = std::fs::read_to_string(path).map_err(|source| Error::PrivateKeyRead {
42 path: path.display().to_string(),
43 source,
44 })?;
45 Self::from_p8_pem(key_id, issuer_id, &pem)
46 }
47
48 /// Load a key from an already-in-memory PEM string.
49 pub fn from_p8_pem(
50 key_id: impl Into<String>,
51 issuer_id: impl Into<String>,
52 pem: &str,
53 ) -> Result<Self> {
54 let encoding_key = EncodingKey::from_ec_pem(pem.as_bytes())?;
55 Ok(Self {
56 key_id: key_id.into(),
57 issuer_id: issuer_id.into(),
58 encoding_key,
59 lifetime_secs: MAX_TOKEN_LIFETIME_SECS,
60 })
61 }
62
63 /// Override the token lifetime in seconds. Values above Apple's 20
64 /// minute ceiling are clamped down to it.
65 pub fn with_lifetime_secs(mut self, secs: u64) -> Self {
66 self.lifetime_secs = secs.min(MAX_TOKEN_LIFETIME_SECS);
67 self
68 }
69
70 pub fn key_id(&self) -> &str {
71 &self.key_id
72 }
73
74 pub fn lifetime_secs(&self) -> u64 {
75 self.lifetime_secs
76 }
77
78 /// Mint a fresh bearer token. `Client` caches and reuses the result of
79 /// this until shortly before it expires rather than calling it per
80 /// request.
81 pub fn token(&self) -> Result<String> {
82 let now = SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .unwrap_or_default() // a clock before 1970 isn't a real failure mode worth propagating
85 .as_secs();
86
87 let claims = Claims {
88 iss: self.issuer_id.clone(),
89 iat: now,
90 exp: now + self.lifetime_secs,
91 aud: AUDIENCE.to_string(),
92 };
93
94 let mut header = Header::new(Algorithm::ES256);
95 header.kid = Some(self.key_id.clone());
96
97 Ok(encode(&header, &claims, &self.encoding_key)?)
98 }
99 }