sign_in_with_apple_fixed/
lib.rs1#![forbid(unsafe_code)]
2#![deny(clippy::pedantic)]
3#![deny(clippy::unwrap_used)]
4#![deny(clippy::panic)]
5#![deny(clippy::perf)]
6#![deny(clippy::nursery)]
7#![deny(clippy::match_like_matches_macro)]
8#![allow(clippy::module_name_repetitions)]
9#![allow(clippy::missing_errors_doc)]
10
11mod data;
12mod error;
13
14pub use data::{Claims, ClaimsServer2Server};
15pub use error::Error;
16
17use data::{KeyComponents, APPLE_ISSUER, APPLE_PUB_KEYS};
18use error::Result;
19use hyper::{body, Body, Client, Request};
20use hyper_tls::HttpsConnector;
21use jsonwebtoken::{
22 self, decode, decode_header, DecodingKey, TokenData, Validation,
23};
24use serde::de::DeserializeOwned;
25use std::collections::HashMap;
26
27async fn fetch_apple_keys() -> Result<HashMap<String, KeyComponents>>
28{
29 let https = HttpsConnector::new();
30 let client = Client::builder().build::<_, hyper::Body>(https);
31
32 let req = Request::builder()
33 .method("GET")
34 .uri(APPLE_PUB_KEYS)
35 .body(Body::from(""))?;
36
37 let resp = client.request(req).await?;
38 let buf = body::to_bytes(resp).await?;
39
40 let mut resp: HashMap<String, Vec<KeyComponents>> =
41 serde_json::from_slice(&buf)?;
42
43 resp.remove("keys").map_or(Err(Error::AppleKeys), |res| {
44 Ok(res
45 .into_iter()
46 .map(|val| (val.kid.clone(), val))
47 .collect::<HashMap<String, KeyComponents>>())
48 })
49}
50
51pub async fn decode_token<T: DeserializeOwned>(
53 token: String,
54 ignore_expire: bool,
55) -> Result<TokenData<T>> {
56 let header = decode_header(token.as_str())?;
57
58 let kid = match header.kid {
59 Some(k) => k,
60 None => return Err(Error::KidNotFound),
61 };
62
63 let pubkeys = fetch_apple_keys().await?;
64
65 let pubkey = match pubkeys.get(&kid) {
66 Some(key) => key,
67 None => return Err(Error::KeyNotFound),
68 };
69
70 let mut val = Validation::new(header.alg);
71 val.validate_exp = !ignore_expire;
72 let decoding_key =
73 &DecodingKey::from_rsa_components(&pubkey.n, &pubkey.e)?;
74
75 let token_data = decode::<T>(token.as_str(), decoding_key, &val)
76 .map_err(|err| {
77 println!("this is error : {err:?}");
78 err
79 })?;
80
81 Ok(token_data)
82}
83
84pub async fn validate(
85 client_id: String,
86 token: String,
87 ignore_expire: bool,
88) -> Result<TokenData<Claims>> {
89 let token_data =
90 decode_token::<Claims>(token, ignore_expire).await?;
91
92 if token_data.claims.iss != APPLE_ISSUER {
94 return Err(Error::IssClaimMismatch);
95 }
96
97 if token_data.claims.aud != client_id {
98 return Err(Error::ClientIdMismatch);
99 }
100 Ok(token_data)
101}
102
103#[must_use]
105pub fn is_expired(
106 validate_result: &Result<TokenData<Claims>>,
107) -> bool {
108 if let Err(Error::Jwt(error)) = validate_result {
109 return matches!(
110 error.kind(),
111 jsonwebtoken::errors::ErrorKind::ExpiredSignature
112 );
113 }
114
115 false
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::{
121 decode_token, is_expired, validate, ClaimsServer2Server,
122 Error,
123 };
124
125 #[tokio::test]
126 async fn validate_test() -> std::result::Result<(), Error> {
127 let token = "eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoidG93bi5waWVjZS5hcHAiLCJleHAiOjE2NTM3MjU1MjYsImlhdCI6MTY1MzYzOTEyNiwic3ViIjoiMDAwNDIyLjJkMWNlODE2Njk2ZTRkYTBiMjhhOTk3ZmJkYTBiYzU5LjA5MzEiLCJhdF9oYXNoIjoidVFGWTBVMmdjTkhBRzlacjluZ0hGdyIsImVtYWlsIjoidXN3dXJpa2lqaUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJhdXRoX3RpbWUiOjE2NTM2MzkxMDEsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.i3Dp01s6RGc5NBu97Vw-VdvNi6ejilME1m1e-27Lv2P7nKUPUos2HJb888oiQRroC7E3zihDAL53FbsFp7kgGDVTt9R68YKdaM-Nwl97ywUP9ehVk1KuUd9rd4cHEN8Cms7YnJErSMIOmj3mMjg6ISEGQHrOPVtG9fk_9HqK7mcyxtnsAM9K-CxGbwzgVqJBgQK45qBq-lNPYnOJOKO6DQfOA86X0csYZ2wqFlc89Z3APOkL_Q_Y69ERq1YHyRg4IfW9puTURhjWRNpW_7Qt4RhP4ewWRKsJ1fr_E64bbpnLFyepJLBHYePNiEbfZfd0k_crdSS4_fuzHWHFsDqddg";
128
129 let _result = validate(
130 "town.piece.app".to_string(),
131 token.to_string(),
132 true,
133 )
134 .await?;
135
136 Ok(())
137 }
138
139 #[ignore]
140 #[tokio::test]
141 async fn validate_expired() {
142 let token = "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmdhbWVyb2FzdGVycy5zdGFjazQiLCJleHAiOjE2MzA4Mjc4MzAsImlhdCI6MTYzMDc0MTQzMCwic3ViIjoiMDAxMDI2LjE2MTEyYjM2Mzc4NDQwZDk5NWFmMjJiMjY4ZjAwOTg0LjE3NDQiLCJjX2hhc2giOiI0QjZKWTU4TmstVUJsY3dMa2VLc2lnIiwiYXV0aF90aW1lIjoxNjMwNzQxNDMwLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.iW0xk__fPD0mlh9UU-vh9VnR8yekWq64sl5re5d7UmDJxb1Fzk1Kca-hkA_Ka1LhSmKADdFW0DYEZhckqh49DgFtFdx6hM9t7guK3yrvBglhF5LAyb8NR028npxioLTTIgP_aR6Bpy5AyLQrU-yYEx2WTPYV5ln9n8vW154gZKRyl2KBlj9fS11BL_X1UFbFrL21GG_iPbB4qt5ywwTPoJ-diGN5JQzP5fk4yU4e4YmHhxJrT0NTTux2mB3lGJLa6YN-JYe_BuVV9J-sg_2r_ugTOUp3xQpfntu8xgQrY5W0oPxAPM4sibNLsye2kgPYYxfRYowc0JIjOcOd_JHDbQ";
143
144 let res = validate(
145 "com.gameroasters.stack4".into(),
146 token.to_string(),
147 false,
148 )
149 .await;
150
151 assert!(is_expired(&res));
152 }
153
154 #[ignore]
155 #[tokio::test]
156 async fn test_server_to_server_payload() {
157 let token = "eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoidG93bi5waWVjZS5hcHAiLCJleHAiOjE2NTM3MjU1MjYsImlhdCI6MTY1MzYzOTEyNiwic3ViIjoiMDAwNDIyLjJkMWNlODE2Njk2ZTRkYTBiMjhhOTk3ZmJkYTBiYzU5LjA5MzEiLCJhdF9oYXNoIjoidVFGWTBVMmdjTkhBRzlacjluZ0hGdyIsImVtYWlsIjoidXN3dXJpa2lqaUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJhdXRoX3RpbWUiOjE2NTM2MzkxMDEsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.i3Dp01s6RGc5NBu97Vw-VdvNi6ejilME1m1e-27Lv2P7nKUPUos2HJb888oiQRroC7E3zihDAL53FbsFp7kgGDVTt9R68YKdaM-Nwl97ywUP9ehVk1KuUd9rd4cHEN8Cms7YnJErSMIOmj3mMjg6ISEGQHrOPVtG9fk_9HqK7mcyxtnsAM9K-CxGbwzgVqJBgQK45qBq-lNPYnOJOKO6DQfOA86X0csYZ2wqFlc89Z3APOkL_Q_Y69ERq1YHyRg4IfW9puTURhjWRNpW_7Qt4RhP4ewWRKsJ1fr_E64bbpnLFyepJLBHYePNiEbfZfd0k_crdSS4_fuzHWHFsDqddg";
158
159 let result = decode_token::<ClaimsServer2Server>(
160 token.to_string(),
161 true,
162 )
163 .await
164 .unwrap();
165
166 assert_eq!(result.claims.aud, "town.piece.app");
167 assert_eq!(
168 result.claims.events.sub,
169 "000422.2d1ce816696e4da0b28a997fbda0bc59.0931"
170 );
171
172 println!("{:?}", result);
173 }
174}