apple_siwa_client_secret/
lib.rs

1//! [Doc](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)
2
3use std::{error, fmt, time::Duration};
4
5use chrono::{serde::ts_seconds, DateTime, Duration as ChronoDuration, Utc};
6use jwt::{AlgorithmType, Error as JwtError, Header, PKeyWithDigest, SignWithKey, Token};
7use openssl::{ec::EcKey, error::ErrorStack as OpensslErrorStack, hash::MessageDigest, pkey::PKey};
8use serde::{Deserialize, Serialize};
9
10pub const AUDIENCE: &str = "https://appleid.apple.com";
11// 6 months
12pub const EXPIRATION_TIME_DURATION_SECONDS_MAX: u64 = 15777000;
13
14//
15#[derive(Serialize, Deserialize, Debug, Clone)]
16pub struct Claims {
17    pub iss: String,
18    #[serde(with = "ts_seconds")]
19    pub iat: DateTime<Utc>,
20    #[serde(with = "ts_seconds")]
21    pub exp: DateTime<Utc>,
22    pub aud: String,
23    pub sub: String,
24}
25
26pub fn create(
27    key_id: impl AsRef<str>,
28    p8_auth_key_bytes: impl AsRef<[u8]>,
29    team_id: impl AsRef<str>,
30    client_id: impl AsRef<str>,
31    issued_at: impl Into<Option<DateTime<Utc>>>,
32    expiration_time_dur: impl Into<Option<Duration>>,
33) -> Result<String, CreateError> {
34    // TOOD, PKey::private_key_from_pkcs8 not working
35    let pkey = PKeyWithDigest {
36        digest: MessageDigest::sha256(),
37        key: PKey::from_ec_key(
38            EcKey::private_key_from_pem(p8_auth_key_bytes.as_ref())
39                .map_err(CreateError::MakeEcKeyFailed)?,
40        )
41        .map_err(CreateError::MakePKeyFailed)?,
42    };
43
44    let header = Header {
45        algorithm: AlgorithmType::Es256,
46        key_id: Some(key_id.as_ref().to_owned()),
47        ..Default::default()
48    };
49
50    let issued_at = issued_at.into().unwrap_or_else(Utc::now);
51    let mut expiration_time_dur = expiration_time_dur
52        .into()
53        .unwrap_or_else(|| Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX));
54    if expiration_time_dur.as_secs() > EXPIRATION_TIME_DURATION_SECONDS_MAX {
55        expiration_time_dur = Duration::from_secs(EXPIRATION_TIME_DURATION_SECONDS_MAX);
56    }
57    let expiration_time = issued_at + ChronoDuration::seconds(expiration_time_dur.as_secs() as i64);
58
59    let claims = Claims {
60        iss: team_id.as_ref().to_owned(),
61        iat: issued_at,
62        exp: expiration_time,
63        aud: AUDIENCE.to_owned(),
64        sub: client_id.as_ref().to_owned(),
65    };
66
67    let token = Token::new(header, claims)
68        .sign_with_key(&pkey)
69        .map_err(CreateError::TokenSignFailed)?;
70
71    Ok(token.as_str().to_owned())
72}
73
74#[derive(Debug)]
75pub enum CreateError {
76    MakeEcKeyFailed(OpensslErrorStack),
77    MakePKeyFailed(OpensslErrorStack),
78    TokenSignFailed(JwtError),
79}
80impl fmt::Display for CreateError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{:?}", self)
83    }
84}
85impl error::Error for CreateError {}