use base64::Engine; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::Deserialize; use crate::config::OAuthProviderConfig; use crate::error::AuthError; use crate::features::oauth::types::{OAuthProvider, ProviderIdentity}; #[derive(Debug, Deserialize)] struct AppleTokenResp { access_token: Option, refresh_token: Option, expires_in: Option, id_token: String, } #[derive(Debug, Deserialize)] struct AppleClaims { sub: String, email: Option, email_verified: Option, } fn b64_decode(input: &str) -> Result, AuthError> { base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(input) .map_err(|e| AuthError::Internal(format!("apple b64: {e}"))) } /// Apple returns an RSA public key set at https://appleid.apple.com/auth/keys. #[derive(Debug, Deserialize)] struct AppleKey { kid: String, n: String, e: String, } #[derive(Debug, Deserialize)] struct AppleKeys { keys: Vec, } async fn fetch_key_for_kid(kid: &str) -> Result { let url = "https://appleid.apple.com/auth/keys"; let resp: AppleKeys = reqwest::Client::new() .get(url) .send() .await .map_err(|e| AuthError::Internal(format!("apple keys: {e}")))? .error_for_status() .map_err(|e| AuthError::Internal(format!("apple keys status: {e}")))? .json() .await .map_err(|e| AuthError::Internal(format!("apple keys parse: {e}")))?; let k = resp .keys .into_iter() .find(|k| k.kid == kid) .ok_or_else(|| AuthError::Internal("apple signing key not found".into()))?; let n = b64_decode(&k.n)?; let e = b64_decode(&k.e)?; DecodingKey::from_rsa_components( std::str::from_utf8(&n).map_err(|e| AuthError::Internal(format!("apple n utf8: {e}")))?, std::str::from_utf8(&e).map_err(|e| AuthError::Internal(format!("apple e utf8: {e}")))?, ) .map_err(|e| AuthError::Internal(format!("apple decoding key: {e}"))) } fn apple_kid_from_token(token: &str) -> Result { let mut parts = token.split('.'); let _header = parts .next() .ok_or_else(|| AuthError::BadRequest("apple id_token malformed".into()))?; let payload = parts .next() .ok_or_else(|| AuthError::BadRequest("apple id_token malformed".into()))?; let _ = parts.next(); let bytes = b64_decode(payload)?; let v: serde_json::Value = serde_json::from_slice(&bytes) .map_err(|e| AuthError::Internal(format!("apple id_token json: {e}")))?; let kid = v .get("kid") .and_then(|x| x.as_str()) .ok_or_else(|| AuthError::BadRequest("apple id_token missing kid".into()))?; Ok(kid.to_string()) } /// Build an ES256-signed `client_secret` JWT for Apple's `/auth/token` /// endpoint. Apple requires: /// header: alg=ES256, kid= /// payload: iss=, iat, exp, aud=https://appleid.apple.com, sub= /// The signing key is supplied via the provider config's `extra` map under /// `es256_private_key` (PEM, PKCS#8). fn apple_client_secret(cfg: &OAuthProviderConfig) -> Result { let pem = cfg.extra.get("es256_private_key").ok_or_else(|| { AuthError::Internal("apple provider missing es256_private_key in extra".into()) })?; let kid = cfg .extra .get("key_id") .ok_or_else(|| AuthError::Internal("apple provider missing key_id in extra".into()))?; let team = cfg .extra .get("team_id") .ok_or_else(|| AuthError::Internal("apple provider missing team_id in extra".into()))?; let key = EncodingKey::from_ec_pem(pem.as_bytes()) .map_err(|e| AuthError::Internal(format!("apple es256 key: {e}")))?; let now = time::OffsetDateTime::now_utc().unix_timestamp(); let mut header = Header::new(Algorithm::ES256); header.kid = Some(kid.clone()); let claims = serde_json::json!({ "iss": team, "iat": now, "exp": now + 60 * 60, // 1 hour "aud": "https://appleid.apple.com", "sub": cfg.client_id, }); encode(&header, &claims, &key) .map_err(|e| AuthError::Internal(format!("apple client_secret encode: {e}"))) } /// Exchange an Apple authorization code for tokens. Decode the `id_token` /// (RS256) to extract `sub` / `email` / `email_verified`. pub async fn fetch_apple_identity( code: &str, code_verifier: &str, cfg: &OAuthProviderConfig, client: &reqwest::Client, ) -> Result { let client_secret = apple_client_secret(cfg)?; let token: AppleTokenResp = client .post(&cfg.token_url) .form(&[ ("code", code), ("client_id", &cfg.client_id), ("client_secret", &client_secret), ("grant_type", "authorization_code"), ("code_verifier", code_verifier), ]) .send() .await .map_err(|e| AuthError::Internal(format!("apple token: {e}")))? .error_for_status() .map_err(|e| AuthError::Internal(format!("apple token status: {e}")))? .json() .await .map_err(|e| AuthError::Internal(format!("apple token parse: {e}")))?; let kid = apple_kid_from_token(&token.id_token)?; let decoding = fetch_key_for_kid(&kid).await?; let mut validation = Validation::new(Algorithm::RS256); validation.set_audience(std::slice::from_ref(&cfg.client_id)); validation.set_issuer(&["https://appleid.apple.com"]); let data = decode::(&token.id_token, &decoding, &validation) .map_err(|e| AuthError::Internal(format!("apple id_token decode: {e}")))?; let claims = data.claims; Ok(ProviderIdentity { provider: OAuthProvider::Apple, provider_user_id: claims.sub, provider_username: String::new(), email: claims.email, email_verified: claims.email_verified.unwrap_or(false), access_token: token.access_token.unwrap_or_default(), refresh_token: token.refresh_token, expires_at: token .expires_in .map(|secs| time::OffsetDateTime::now_utc() + time::Duration::seconds(secs)), scopes: cfg.scopes.clone(), }) }