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 mut 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;
56
57pub use error::AppleJwtError;
58
59use jsonwebtoken::{errors::ErrorKind, jwk::JwkSet, Algorithm, DecodingKey, Validation};
60use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
61use serde_repr::{Deserialize_repr, Serialize_repr};
62
63const KEYS_URL: &str = "https://appleid.apple.com/auth/keys";
64
65/// Indicates whether the user appears to be a real person.
66/// Apple recommends using this to mitigate fraud.
67#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize_repr, Deserialize_repr)]
68#[repr(u8)]
69pub enum RealUserStatus {
70    Unsupported = 0,
71    Unknown = 1,
72    LikelyReal = 2,
73}
74
75/// Contains the extracted information from a valid JWT
76#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct JwtPayload {
78    /// App bundle id
79    #[serde(rename = "aud")]
80    pub audience: String,
81    pub auth_time: Option<u64>,
82    pub c_hash: Option<String>,
83    /// A string value that represents the user’s email address.
84    /// The email address is either the user’s real email address or the proxy address,
85    /// depending on their private email relay service. This value may be empty for
86    /// Sign in with Apple at Work & School users. For example, younger students may
87    /// not have an email address.
88    pub email: Option<String>,
89    /// Indicates whether Apple verifies the email.
90    /// The system may not verify email addresses for Sign in with Apple at Work & School users
91    #[serde(deserialize_with = "deserialize_bool", default)]
92    pub email_verified: Option<bool>,
93    /// Indicates whether the email that the user shares is the proxy address.
94    #[serde(deserialize_with = "deserialize_bool", default)]
95    pub is_private_email: Option<bool>,
96    /// The time that the identity token expires, in number of seconds since the Unix epoch in UTC.
97    /// Validated that the value is greater than the current date
98    #[serde(rename = "exp")]
99    pub expiration_time: Option<u64>,
100    /// The time that Apple issued the identity token, in number of seconds since the Unix epoch in UTC.
101    #[serde(rename = "iat")]
102    pub issued_at: u64,
103    /// Token issuer, the value is validated to be `https://appleid.apple.com`.
104    #[serde(rename = "iss")]
105    pub issuer: String,
106    /// Indicates whether the user appears to be a real person.
107    /// 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.
108    pub real_user_status: Option<RealUserStatus>,
109    /// Unique identifier for the user:
110    /// - A unique, stable string, serves as the primary identifier of the user
111    /// - Uses the same identifier across all of the apps in the development team associated with an Apple Developer account
112    /// - Differs for the same user across different development teams, and can’t identify a user across development teams
113    /// - Doesn’t change if the user stops using Sign in with Apple with an app and later starts using it again
114    /// - Typically stores alongside the user’s primary key in a database
115    #[serde(rename = "sub")]
116    pub user_id: String,
117
118    /// The user's name, if requested.
119    pub name: Option<AppleName>,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct AppleName {
124    pub first_name: Option<String>,
125    pub last_name: Option<String>,
126}
127
128#[derive(Clone, Debug)]
129pub struct AppleJwtClient {
130    keyset_cache: Option<JwkSet>,
131    validation: Validation,
132}
133
134impl AppleJwtClient {
135    pub fn new<T: ToString>(app_bundle_ids: &[T]) -> Self {
136        let mut validation = Validation::new(Algorithm::RS256);
137        validation.set_audience(app_bundle_ids);
138        validation.set_issuer(&["https://appleid.apple.com"]);
139        validation.set_required_spec_claims(&["exp", "sub", "iss", "aud"]);
140
141        Self {
142            keyset_cache: None,
143            validation,
144        }
145    }
146
147    /// Validate and decode Apple identity JWT
148    pub async fn decode(&mut self, identity_token: &str) -> Result<JwtPayload, AppleJwtError> {
149        let header = jsonwebtoken::decode_header(identity_token)?;
150
151        let Some(key_id) = header.kid else {
152            return Err(AppleJwtError::MissingKeyId);
153        };
154
155        let mut res;
156
157        loop {
158            let (just_loaded, keyset) = self.take_cached_keyset().await?;
159
160            res = Self::try_decode(&key_id, &keyset, identity_token, &self.validation);
161
162            let is_keyset_error = match res {
163                Err(ref e) => match e {
164                    AppleJwtError::MissingJwk(_) => true,
165                    AppleJwtError::JwtError(e) => matches!(
166                        e.kind(),
167                        ErrorKind::InvalidEcdsaKey
168                            | ErrorKind::InvalidRsaKey(_)
169                            | ErrorKind::InvalidAlgorithmName
170                            | ErrorKind::InvalidKeyFormat
171                    ),
172                    _ => false,
173                },
174                _ => false,
175            };
176
177            if just_loaded || res.is_ok() || !is_keyset_error {
178                self.keyset_cache = Some(keyset);
179
180                break;
181            }
182        }
183
184        res
185    }
186
187    fn try_decode(
188        kid: &str,
189        keyset: &JwkSet,
190        token: &str,
191        validation: &Validation,
192    ) -> Result<JwtPayload, AppleJwtError> {
193        let Some(jwk) = keyset.find(kid) else {
194            return Err(AppleJwtError::MissingJwk(kid.to_string()));
195        };
196
197        let key = DecodingKey::from_jwk(jwk)?;
198
199        let token = jsonwebtoken::decode::<JwtPayload>(token, &key, validation)?;
200
201        Ok(token.claims)
202    }
203
204    async fn take_cached_keyset(&mut self) -> Result<(bool, JwkSet), AppleJwtError> {
205        if let Some(keyset) = self.keyset_cache.take() {
206            return Ok((false, keyset));
207        }
208
209        let keyset = reqwest::get(KEYS_URL).await?.json::<JwkSet>().await?;
210
211        Ok((true, keyset))
212    }
213}
214
215fn deserialize_bool<'de, D: Deserializer<'de>>(data: D) -> Result<Option<bool>, D::Error> {
216    data.deserialize_option(AppleOptionalBool)
217}
218
219struct AppleOptionalBool;
220
221impl<'de> Visitor<'de> for AppleOptionalBool {
222    type Value = Option<bool>;
223
224    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
225        formatter.write_str("null or a string (\"true\" or \"false\") or a bool (true or false)")
226    }
227
228    fn visit_none<E>(self) -> Result<Self::Value, E>
229    where
230        E: serde::de::Error,
231    {
232        Ok(None)
233    }
234
235    fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
236    where
237        D: Deserializer<'de>,
238    {
239        Ok(Some(d.deserialize_any(AppleBool)?))
240    }
241}
242
243struct AppleBool;
244
245impl<'de> Visitor<'de> for AppleBool {
246    type Value = bool;
247
248    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
249        formatter.write_str("a string (\"true\" or \"false\") or a bool (true or false)")
250    }
251
252    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
253    where
254        E: serde::de::Error,
255    {
256        Ok(v)
257    }
258
259    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
260    where
261        E: serde::de::Error,
262    {
263        Ok(v == "true")
264    }
265
266    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
267    where
268        E: serde::de::Error,
269    {
270        Ok(v == "true")
271    }
272
273    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
274    where
275        E: serde::de::Error,
276    {
277        Ok(v == "true")
278    }
279}