apple_signin/
lib.rs

1//! # Apple Sign-In
2//!
3//! This crate provides an API to verify and decode Apple's identity JWT. The token is typically generated via
4//! [ASAuthorizationController](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller)
5//! from the [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices) iOS framework.
6//!
7//! This crate validates the `identityToken` instance property present in the
8//! [ASAuthorizationAppleIDCredential](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential) class.
9//!
10//! Currently this crate doesn't support fetching and validating identity tokens via the `authorizationCode` provided in
11//! [ASAuthorizationAppleIDCredential](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential)
12//!
13//! To implement Sign In with Apple:
14//! - You have to have a valid, paid Apple developer account.
15//! - Generate an identifier in <https://developer.apple.com/account/resources/identifiers/list> (eg. `com.example.myapp`)
16//! - Make sure `Sign In with Apple` Capability is enabled on that identifier.
17//! - Configure your app in Xcode to use that identifier as bundle identifier.
18//! - Enable the `Sign In with Apple` Capability in Xcode as well.
19//! - An `identityToken` generated with the `AuthenticationServices` framework can be sent to a backend server for validation.
20//! - Use this crate to validate and decode the token.
21//!
22//! Apple will only provide the email field (and name if requested) the first time you test Sign In with Apple in the simulator with your account.
23//! Subsequent authorization requests on iOS will only yeld the [user id](JwtPayload::user_id) field.
24//!
25//! To get the email field again:
26//! 1. Go to Settings, then tap your name.
27//! 1. Tap Sign-In & Security, then tap Sign in with Apple.
28//! 1. Select the app or developer, then tap Stop Using Apple ID.
29//! 1. You may need to restart the simulator or device
30//!
31//! ## Usage
32//! Create a new client and configure it with your app bundle id(s).
33//!
34//! ```
35//! use apple_signin::AppleJwtClient;
36//!
37//! #[tokio::main]
38//! async fn main() -> Result<()> {
39//!     let client = AppleJwtClient::new(&["com.example.myapp"]);
40//!     let payload = client.decode("[IDENTITY TOKEN]").await?;
41//!
42//!     dbg!(payload);
43//!
44//!     Ok(())
45//! }
46//! ```
47//!
48//! ## Caching
49//!
50//! It is recommended to keep the client instance around and not create a new one on every validation request.
51//! The client will fetch and cache JWT keys provided by Apple from <https://appleid.apple.com/auth/keys>.
52//! Only if the cached keys stop working will the client try to fetch new ones.
53//!
54mod error;
55use std::fmt;
56use std::sync::Arc;
57
58pub use error::AppleJwtError;
59
60use async_lock::RwLock;
61use jsonwebtoken::{errors::ErrorKind, jwk::JwkSet, Algorithm, DecodingKey, Validation};
62use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
63use serde_repr::{Deserialize_repr, Serialize_repr};
64
65const KEYS_URL: &str = "https://appleid.apple.com/auth/keys";
66
67/// Indicates whether the user appears to be a real person.
68/// Apple recommends using this to mitigate fraud.
69#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize_repr, Deserialize_repr)]
70#[repr(u8)]
71pub enum RealUserStatus {
72    Unsupported = 0,
73    Unknown = 1,
74    LikelyReal = 2,
75}
76
77/// Contains the extracted information from a valid JWT
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct JwtPayload {
80    /// App bundle id
81    #[serde(rename = "aud")]
82    pub audience: String,
83    pub auth_time: Option<u64>,
84    pub c_hash: Option<String>,
85    /// A string value that represents the user’s email address.
86    /// The email address is either the user’s real email address or the proxy address,
87    /// depending on their private email relay service. This value may be empty for
88    /// Sign in with Apple at Work & School users. For example, younger students may
89    /// not have an email address.
90    pub email: Option<String>,
91    /// Indicates whether Apple verifies the email.
92    /// The system may not verify email addresses for Sign in with Apple at Work & School users
93    #[serde(deserialize_with = "deserialize_bool", default)]
94    pub email_verified: Option<bool>,
95    /// Indicates whether the email that the user shares is the proxy address.
96    #[serde(deserialize_with = "deserialize_bool", default)]
97    pub is_private_email: Option<bool>,
98    /// The time that the identity token expires, in number of seconds since the Unix epoch in UTC.
99    /// Validated that the value is greater than the current date
100    #[serde(rename = "exp")]
101    pub expiration_time: Option<u64>,
102    /// The time that Apple issued the identity token, in number of seconds since the Unix epoch in UTC.
103    #[serde(rename = "iat")]
104    pub issued_at: u64,
105    /// Token issuer, the value is validated to be `https://appleid.apple.com`.
106    #[serde(rename = "iss")]
107    pub issuer: String,
108    /// Indicates whether the user appears to be a real person.
109    /// This field is present only in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14 and later. The claim isn’t present or supported for web-based apps.
110    pub real_user_status: Option<RealUserStatus>,
111    /// Unique identifier for the user:
112    /// - A unique, stable string, serves as the primary identifier of the user
113    /// - Uses the same identifier across all of the apps in the development team associated with an Apple Developer account
114    /// - Differs for the same user across different development teams, and can’t identify a user across development teams
115    /// - Doesn’t change if the user stops using Sign in with Apple with an app and later starts using it again
116    /// - Typically stores alongside the user’s primary key in a database
117    #[serde(rename = "sub")]
118    pub user_id: String,
119
120    /// The user's name, if requested.
121    pub name: Option<AppleName>,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct AppleName {
126    pub first_name: Option<String>,
127    pub last_name: Option<String>,
128}
129
130#[derive(Clone, Debug)]
131pub struct AppleJwtClient {
132    keyset_cache: Arc<RwLock<Option<JwkSet>>>,
133    validation: Validation,
134}
135
136impl AppleJwtClient {
137    pub fn new<T: ToString>(app_bundle_ids: &[T]) -> Self {
138        let mut validation = Validation::new(Algorithm::RS256);
139        validation.set_audience(app_bundle_ids);
140        validation.set_issuer(&["https://appleid.apple.com"]);
141        validation.set_required_spec_claims(&["exp", "sub", "iss", "aud"]);
142
143        Self {
144            keyset_cache: Arc::new(RwLock::new(None)),
145            validation,
146        }
147    }
148
149    /// Validate and decode Apple identity JWT
150    pub async fn decode(&self, identity_token: &str) -> Result<JwtPayload, AppleJwtError> {
151        let header = jsonwebtoken::decode_header(identity_token)?;
152
153        let Some(key_id) = header.kid else {
154            return Err(AppleJwtError::MissingKeyId);
155        };
156
157        let mut res;
158
159        loop {
160            let (just_loaded, keyset) = self.take_cached_keyset().await?;
161
162            res = Self::try_decode(&key_id, &keyset, identity_token, &self.validation);
163
164            let is_keyset_error = match res {
165                Err(ref e) => match e {
166                    AppleJwtError::MissingJwk(_) => true,
167                    AppleJwtError::JwtError(e) => matches!(
168                        e.kind(),
169                        ErrorKind::InvalidEcdsaKey
170                            | ErrorKind::InvalidRsaKey(_)
171                            | ErrorKind::InvalidAlgorithmName
172                            | ErrorKind::InvalidKeyFormat
173                    ),
174                    _ => false,
175                },
176                _ => false,
177            };
178
179            if just_loaded || res.is_ok() || !is_keyset_error {
180                *self.keyset_cache.write().await = Some(keyset);
181
182                break;
183            }
184        }
185
186        res
187    }
188
189    fn try_decode(
190        kid: &str,
191        keyset: &JwkSet,
192        token: &str,
193        validation: &Validation,
194    ) -> Result<JwtPayload, AppleJwtError> {
195        let Some(jwk) = keyset.find(kid) else {
196            return Err(AppleJwtError::MissingJwk(kid.to_string()));
197        };
198
199        let key = DecodingKey::from_jwk(jwk)?;
200
201        let token = jsonwebtoken::decode::<JwtPayload>(token, &key, validation)?;
202
203        Ok(token.claims)
204    }
205
206    async fn take_cached_keyset(&self) -> Result<(bool, JwkSet), AppleJwtError> {
207        if let Some(keyset) = self.keyset_cache.write().await.take() {
208            return Ok((false, keyset));
209        }
210
211        let keyset = reqwest::get(KEYS_URL).await?.json::<JwkSet>().await?;
212
213        Ok((true, keyset))
214    }
215}
216
217fn deserialize_bool<'de, D: Deserializer<'de>>(data: D) -> Result<Option<bool>, D::Error> {
218    data.deserialize_option(AppleOptionalBool)
219}
220
221struct AppleOptionalBool;
222
223impl<'de> Visitor<'de> for AppleOptionalBool {
224    type Value = Option<bool>;
225
226    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
227        formatter.write_str("null or a string (\"true\" or \"false\") or a bool (true or false)")
228    }
229
230    fn visit_none<E>(self) -> Result<Self::Value, E>
231    where
232        E: serde::de::Error,
233    {
234        Ok(None)
235    }
236
237    fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
238    where
239        D: Deserializer<'de>,
240    {
241        Ok(Some(d.deserialize_any(AppleBool)?))
242    }
243}
244
245struct AppleBool;
246
247impl<'de> Visitor<'de> for AppleBool {
248    type Value = bool;
249
250    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
251        formatter.write_str("a string (\"true\" or \"false\") or a bool (true or false)")
252    }
253
254    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
255    where
256        E: serde::de::Error,
257    {
258        Ok(v)
259    }
260
261    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
262    where
263        E: serde::de::Error,
264    {
265        Ok(v == "true")
266    }
267
268    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
269    where
270        E: serde::de::Error,
271    {
272        Ok(v == "true")
273    }
274
275    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
276    where
277        E: serde::de::Error,
278    {
279        Ok(v == "true")
280    }
281}