Skip to main content

apple_search_ads_client_secret/
lib.rs

1//! [Doc](https://developer.apple.com/documentation/apple_search_ads/implementing_oauth_for_the_apple_search_ads_api)
2
3use 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 openssl::{ec::EcKey, error::ErrorStack as OpensslErrorStack, pkey::PKey};
8use serde::{Deserialize, Serialize};
9
10pub const AUDIENCE: &str = "https://appleid.apple.com";
11// 180 days
12pub const EXPIRATION_TIME_DURATION_SECONDS_MAX: u64 = 86400 * 180;
13
14const EC_PRIVATE_KEY_BEGIN: &[u8] = b"-----BEGIN EC PRIVATE KEY-----";
15
16//
17#[derive(Serialize, Deserialize, Debug, Clone)]
18pub struct Claims {
19    pub iss: Box<str>,
20    #[serde(with = "ts_seconds")]
21    pub iat: DateTime<Utc>,
22    #[serde(with = "ts_seconds")]
23    pub exp: DateTime<Utc>,
24    pub aud: Box<str>,
25    pub sub: Box<str>,
26}
27
28pub fn create(
29    key_id: impl AsRef<str>,
30    ec_private_key_pem_bytes: impl AsRef<[u8]>,
31    team_id: impl AsRef<str>,
32    client_id: impl AsRef<str>,
33    issued_at: impl Into<Option<DateTime<Utc>>>,
34    expiration_time_dur: impl Into<Option<Duration>>,
35) -> Result<Box<str>, CreateError> {
36    let ec_private_key_pem_bytes = ec_private_key_pem_bytes.as_ref();
37
38    let key = if ec_private_key_pem_bytes
39        .windows(EC_PRIVATE_KEY_BEGIN.len())
40        .any(|x| x == EC_PRIVATE_KEY_BEGIN)
41    {
42        let pem_bytes = PKey::from_ec_key(
43            EcKey::private_key_from_pem(ec_private_key_pem_bytes)
44                .map_err(CreateError::MakeEcKeyFailed)?,
45        )
46        .map_err(CreateError::MakePKeyFailed)?
47        .private_key_to_pem_pkcs8()
48        .map_err(CreateError::ToPemPkcs8Failed)?;
49
50        EncodingKey::from_ec_pem(&pem_bytes).map_err(CreateError::MakeEncodingKeyFailed)?
51    } else {
52        EncodingKey::from_ec_pem(ec_private_key_pem_bytes)
53            .map_err(CreateError::MakeEncodingKeyFailed)?
54    };
55
56    let mut header = Header::new(Algorithm::ES256);
57    header.typ = None;
58    header.kid = Some(key_id.as_ref().to_owned());
59
60    let issued_at = issued_at.into().unwrap_or_else(Utc::now);
61    let mut expiration_time_dur = expiration_time_dur
62        .into()
63        .unwrap_or_else(|| Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX));
64    if expiration_time_dur.as_secs() > EXPIRATION_TIME_DURATION_SECONDS_MAX {
65        expiration_time_dur = Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX);
66    }
67    let expiration_time = issued_at + ChronoDuration::seconds(expiration_time_dur.as_secs() as i64);
68
69    let claims = Claims {
70        iss: team_id.as_ref().into(),
71        iat: issued_at,
72        exp: expiration_time,
73        aud: AUDIENCE.into(),
74        sub: client_id.as_ref().into(),
75    };
76
77    let token = encode(&header, &claims, &key).map_err(CreateError::EncodeFailed)?;
78
79    Ok(token.as_str().into())
80}
81
82#[derive(Debug)]
83pub enum CreateError {
84    MakeEcKeyFailed(OpensslErrorStack),
85    MakePKeyFailed(OpensslErrorStack),
86    ToPemPkcs8Failed(OpensslErrorStack),
87    MakeEncodingKeyFailed(JsonwebtokenError),
88    EncodeFailed(JsonwebtokenError),
89}
90impl fmt::Display for CreateError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{:?}", self)
93    }
94}
95impl std::error::Error for CreateError {}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_create_for_pem() {
103        /*
104        openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
105        */
106        const PEM_PRIVATE_KEY: &str = r#"
107-----BEGIN EC PRIVATE KEY-----
108MHcCAQEEIKtnxllRY8nbndBQwT9we4pEULtjpW605iwvzLlKcBq4oAoGCCqGSM49
109AwEHoUQDQgAEY58v74eQFyLtu5rtCpeU4NggVSUQSOcHhN744t0gWGc/xXkCSusz
110LaZriCQnnqq4Vx+IscLFcrjBj+ulZzKlUQ==
111-----END EC PRIVATE KEY-----
112        "#;
113
114        const CLIENT_ID: &str = "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577";
115        const TEAM_ID: &str = "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577";
116        const KEY_ID: &str = "bacaebda-e219-41ee-a907-e2c25b24d1b2";
117
118        let secret = create(
119            KEY_ID,
120            PEM_PRIVATE_KEY,
121            TEAM_ID,
122            CLIENT_ID,
123            "2022-06-06T00:00:00Z".parse::<DateTime<Utc>>().unwrap(),
124            Duration::from_secs(3600 * 24 * 180),
125        )
126        .unwrap();
127
128        /*
129        eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhY2FlYmRhLWUyMTktNDFlZS1hOTA3LWUyYzI1YjI0ZDFiMiJ9.eyJpc3MiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3IiwiaWF0IjoxNjU0NDczNjAwLCJleHAiOjE2NzAwMjU2MDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3In0.bN3KRWDJft-rjqRbOuuzfsImPT4RPEy01ILYJRBe4v_WJtJdi-7xBpi9UCcSN1WRe3Ozobvou5ruxXjVFnB_6Q
130        */
131
132        println!("{}", secret);
133        let mut split = secret.split('.');
134        assert_eq!(
135            split.next().unwrap(),
136            "eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhY2FlYmRhLWUyMTktNDFlZS1hOTA3LWUyYzI1YjI0ZDFiMiJ9"
137        );
138        assert_eq!(split.next().unwrap() , "eyJpc3MiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3IiwiaWF0IjoxNjU0NDczNjAwLCJleHAiOjE2NzAwMjU2MDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3In0");
139        split.next();
140        assert!(split.next().is_none());
141    }
142
143    #[test]
144    fn test_create_for_pem_pkcs8() {
145        /*
146        openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
147        cat private-key.pem | openssl pkcs8 -topk8 -nocrypt -out private-key-pkcs8.pem
148        */
149        const PEM_PRIVATE_KEY: &str = r#"
150-----BEGIN PRIVATE KEY-----
151MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgq2fGWVFjydud0FDB
152P3B7ikRQu2OlbrTmLC/MuUpwGrihRANCAARjny/vh5AXIu27mu0Kl5Tg2CBVJRBI
1535weE3vji3SBYZz/FeQJK6zMtpmuIJCeeqrhXH4ixwsVyuMGP66VnMqVR
154-----END PRIVATE KEY-----
155        "#;
156
157        const CLIENT_ID: &str = "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577";
158        const TEAM_ID: &str = "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577";
159        const KEY_ID: &str = "bacaebda-e219-41ee-a907-e2c25b24d1b2";
160
161        let secret = create(
162            KEY_ID,
163            PEM_PRIVATE_KEY,
164            TEAM_ID,
165            CLIENT_ID,
166            "2022-06-06T00:00:00Z".parse::<DateTime<Utc>>().unwrap(),
167            Duration::from_secs(3600 * 24 * 180),
168        )
169        .unwrap();
170
171        println!("{}", secret);
172        let mut split = secret.split('.');
173        assert_eq!(
174            split.next().unwrap(),
175            "eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhY2FlYmRhLWUyMTktNDFlZS1hOTA3LWUyYzI1YjI0ZDFiMiJ9"
176        );
177        assert_eq!(split.next().unwrap() , "eyJpc3MiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3IiwiaWF0IjoxNjU0NDczNjAwLCJleHAiOjE2NzAwMjU2MDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3In0");
178        split.next();
179        assert!(split.next().is_none());
180    }
181}