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}