apple_app_store_connect_api_token/
lib.rs1use core::{fmt, time::Duration};
4
5use chrono::{serde::ts_seconds, DateTime, Duration as ChronoDuration, Utc};
6use jsonwebtoken::{encode, errors::Error as JsonwebtokenError, Algorithm, EncodingKey, Header};
7use serde::{Deserialize, Serialize};
8
9pub const AUDIENCE: &str = "appstoreconnect-v1";
10pub const EXPIRATION_TIME_DURATION_SECONDS_MAX: u64 = 60 * 60 * 24 * 6;
12pub const EXPIRATION_TIME_DURATION_SECONDS_MAX_FOR_MOST_REQUESTS: u64 = 60 * 20;
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
17pub struct Claims {
18 pub iss: Box<str>,
19 #[serde(with = "ts_seconds")]
20 pub iat: DateTime<Utc>,
21 #[serde(with = "ts_seconds")]
22 pub exp: DateTime<Utc>,
23 pub aud: Box<str>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub scope: Option<Vec<Box<str>>>,
26}
27
28pub fn create(
29 key_id: impl AsRef<str>,
30 auth_key_p8_bytes: impl AsRef<[u8]>,
31 issuer_id: impl AsRef<str>,
32 scope: impl Into<Option<Vec<Box<str>>>>,
33 issued_at: impl Into<Option<DateTime<Utc>>>,
34 expiration_time_dur: impl Into<Option<Duration>>,
35) -> Result<Box<str>, CreateError> {
36 let key = EncodingKey::from_ec_pem(auth_key_p8_bytes.as_ref())
37 .map_err(CreateError::MakeEncodingKeyFailed)?;
38
39 let mut header = Header::new(Algorithm::ES256);
40 header.typ = Some("JWT".to_owned());
41 header.kid = Some(key_id.as_ref().to_owned());
42
43 let issued_at = issued_at.into().unwrap_or_else(Utc::now);
44 let mut expiration_time_dur = expiration_time_dur.into().unwrap_or_else(|| {
45 Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX_FOR_MOST_REQUESTS)
46 });
47 if expiration_time_dur.as_secs() > EXPIRATION_TIME_DURATION_SECONDS_MAX {
48 expiration_time_dur = Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX);
49 }
50 let expiration_time = issued_at + ChronoDuration::seconds(expiration_time_dur.as_secs() as i64);
51
52 let claims = Claims {
53 iss: issuer_id.as_ref().into(),
54 iat: issued_at,
55 exp: expiration_time,
56 aud: AUDIENCE.into(),
57 scope: scope.into(),
58 };
59
60 let token = encode(&header, &claims, &key).map_err(CreateError::EncodeFailed)?;
61
62 Ok(token.as_str().into())
63}
64
65#[derive(Debug)]
66pub enum CreateError {
67 MakeEncodingKeyFailed(JsonwebtokenError),
68 EncodeFailed(JsonwebtokenError),
69}
70impl fmt::Display for CreateError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "{:?}", self)
73 }
74}
75impl std::error::Error for CreateError {}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn test_create() {
83 const P8_PRIVATE_KEY: &str = r#"
88-----BEGIN PRIVATE KEY-----
89MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsXxkulZ+qopu4M9j
90++u5MRRIYJQG3oobmJsvhKQx4+uhRANCAARIXRezNaTknAu66ifGy0vCkxTWD7oX
91LjUOxL+a+60LCYQsO4O9fzi1klyzxSa3n2ZUvjNkiqlbxufipiYejOZk
92-----END PRIVATE KEY-----
93 "#;
94
95 const ISSUER_ID: &str = "57246542-96fe-1a63-e053-0824d011072a";
96 const KEY_ID: &str = "2X9R4HXF34";
97
98 let secret = create(
99 KEY_ID,
100 P8_PRIVATE_KEY,
101 ISSUER_ID,
102 vec!["GET /v1/apps?filter[platform]=IOS".into()],
103 "2022-06-06T00:00:00Z".parse::<DateTime<Utc>>().unwrap(),
104 Duration::from_secs(60 * 60 * 10),
105 )
106 .unwrap();
107
108 println!("{}", secret);
109 let mut split = secret.split('.');
110 assert_eq!(
111 split.next().unwrap(),
112 "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjJYOVI0SFhGMzQifQ"
113 );
114 assert_eq!(split.next().unwrap() , "eyJpc3MiOiI1NzI0NjU0Mi05NmZlLTFhNjMtZTA1My0wODI0ZDAxMTA3MmEiLCJpYXQiOjE2NTQ0NzM2MDAsImV4cCI6MTY1NDUwOTYwMCwiYXVkIjoiYXBwc3RvcmVjb25uZWN0LXYxIiwic2NvcGUiOlsiR0VUIC92MS9hcHBzP2ZpbHRlcltwbGF0Zm9ybV09SU9TIl19");
115 split.next();
116 assert!(split.next().is_none());
117 }
118}