better_auth_api/plugins/oauth/providers/apple/
client_secret.rs1use std::fmt;
9use std::sync::{Arc, Mutex, PoisonError};
10use std::time::Duration;
11
12use better_auth_core::{AuthError, AuthResult};
13use josekit::JoseError;
14use josekit::jws::alg::ecdsa::EcdsaJwsSigner;
15use josekit::jws::{ES256, JwsHeader};
16use josekit::jwt::{self, JwtPayload};
17use serde_json::{Map, Value};
18
19const AUDIENCE: &str = "https://appleid.apple.com";
20const MAX_LIFETIME_SECS: i64 = 15_777_000;
22const MIN_LIFETIME_SECS: i64 = 600;
24const DEFAULT_LIFETIME_SECS: i64 = 3600;
25const RENEWAL_MARGIN_SECS: i64 = 300;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
30pub enum AppleClientSecretError {
31 #[error("Apple team ID must not be empty")]
33 MissingTeamId,
34 #[error("Apple key ID must not be empty")]
36 MissingKeyId,
37 #[error("Apple private key must be a PKCS #8 PEM key for ES256")]
39 InvalidPrivateKey,
40 #[error("Apple client secret lifetime must be between 600 and 15777000 seconds")]
42 InvalidLifetime,
43}
44
45#[derive(Clone)]
52pub struct AppleClientSecretSigner {
53 inner: Arc<Signer>,
54}
55
56struct Signer {
57 team_id: String,
58 key_id: String,
59 key: EcdsaJwsSigner,
60 lifetime_secs: i64,
61 cache: Mutex<Option<CachedSecret>>,
62}
63
64struct CachedSecret {
65 client_id: String,
66 value: String,
67 issued_at: i64,
68 expires_at: i64,
69}
70
71impl AppleClientSecretSigner {
72 pub fn new(
77 team_id: &str,
78 key_id: &str,
79 private_key_pem: &[u8],
80 ) -> Result<Self, AppleClientSecretError> {
81 let team_id = team_id.trim();
82 if team_id.is_empty() {
83 return Err(AppleClientSecretError::MissingTeamId);
84 }
85 let key_id = key_id.trim();
86 if key_id.is_empty() {
87 return Err(AppleClientSecretError::MissingKeyId);
88 }
89 let key = ES256
91 .signer_from_pem(private_key_pem)
92 .map_err(|_| AppleClientSecretError::InvalidPrivateKey)?;
93 Signer::new(
94 team_id.to_owned(),
95 key_id.to_owned(),
96 key,
97 DEFAULT_LIFETIME_SECS,
98 )
99 .map(|signer| Self {
100 inner: Arc::new(signer),
101 })
102 }
103
104 pub fn with_lifetime(self, lifetime: Duration) -> Result<Self, AppleClientSecretError> {
110 let lifetime_secs = i64::try_from(lifetime.as_secs())
111 .ok()
112 .filter(|seconds| (MIN_LIFETIME_SECS..=MAX_LIFETIME_SECS).contains(seconds))
113 .ok_or(AppleClientSecretError::InvalidLifetime)?;
114 let signer = Signer::new(
115 self.inner.team_id.clone(),
116 self.inner.key_id.clone(),
117 self.inner.key.clone(),
118 lifetime_secs,
119 )?;
120 Ok(Self {
121 inner: Arc::new(signer),
122 })
123 }
124
125 pub fn client_secret(&self, client_id: &str) -> AuthResult<String> {
130 self.client_secret_at(client_id, chrono::Utc::now().timestamp())
131 }
132
133 pub(in crate::plugins::oauth) fn client_secret_at(
134 &self,
135 client_id: &str,
136 now: i64,
137 ) -> AuthResult<String> {
138 let mut cache = self
139 .inner
140 .cache
141 .lock()
142 .unwrap_or_else(PoisonError::into_inner);
143 if let Some(cached) = cache.as_ref().filter(|cached| {
144 cached.client_id == client_id
145 && cached.issued_at <= now
146 && cached.expires_at - now > RENEWAL_MARGIN_SECS
147 }) {
148 return Ok(cached.value.clone());
149 }
150 let value = self
151 .inner
152 .sign(client_id, now)
153 .map_err(|_| AuthError::internal("Failed to sign the Apple client secret"))?;
154 *cache = Some(CachedSecret {
155 client_id: client_id.to_owned(),
156 value: value.clone(),
157 issued_at: now,
158 expires_at: now + self.inner.lifetime_secs,
159 });
160 Ok(value)
161 }
162}
163
164impl fmt::Debug for AppleClientSecretSigner {
165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166 formatter
167 .debug_struct("AppleClientSecretSigner")
168 .field("team_id", &self.inner.team_id)
169 .field("key_id", &self.inner.key_id)
170 .field("lifetime_secs", &self.inner.lifetime_secs)
171 .finish_non_exhaustive()
172 }
173}
174
175impl Signer {
176 fn new(
177 team_id: String,
178 key_id: String,
179 key: EcdsaJwsSigner,
180 lifetime_secs: i64,
181 ) -> Result<Self, AppleClientSecretError> {
182 let signer = Self {
183 team_id,
184 key_id,
185 key,
186 lifetime_secs,
187 cache: Mutex::new(None),
188 };
189 let _ = signer
191 .sign("key-check", 0)
192 .map_err(|_| AppleClientSecretError::InvalidPrivateKey)?;
193 Ok(signer)
194 }
195
196 fn sign(&self, client_id: &str, issued_at: i64) -> Result<String, JoseError> {
197 let mut header = JwsHeader::new();
199 header.set_key_id(&self.key_id);
200 let mut claims = Map::new();
201 let _ = claims.insert("iss".into(), Value::from(self.team_id.as_str()));
202 let _ = claims.insert("iat".into(), Value::from(issued_at));
203 let _ = claims.insert("exp".into(), Value::from(issued_at + self.lifetime_secs));
204 let _ = claims.insert("aud".into(), Value::from(AUDIENCE));
205 let _ = claims.insert("sub".into(), Value::from(client_id));
206 let payload = JwtPayload::from_map(claims)?;
207 jwt::encode_with_signer(&payload, &header, &self.key)
208 }
209}