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}