siwa_async/common/
object.rs

1//! Common Object
2use std::{
3	fmt::Display,
4	time::{Duration, SystemTime},
5};
6
7use jwt::{PKeyWithDigest, SignWithKey, Token};
8use openssl::hash::MessageDigest;
9use serde::{self, Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::error::{Error, Result};
13
14use super::constants::{
15	APPLE_ISSUER, CLIENT_SECRET_VALID_DURATION_MAX,
16};
17
18/// <https://developer.apple.com/documentation/sign_in_with_apple/errorresponse>
19#[derive(Debug, Deserialize, Serialize, PartialEq)]
20pub struct AuthErrorResponse {
21	pub error: AuthError,
22}
23
24#[derive(Debug, Deserialize, Serialize, PartialEq, Error)]
25pub enum AuthError {
26	#[serde(rename = "invalid_request")]
27	InvalidRequest,
28	#[serde(rename = "invalid_client")]
29	InvalidClient,
30	#[serde(rename = "invalid_grant")]
31	InvalidGrant,
32	#[serde(rename = "unauthorized_client")]
33	UnauthorizedClient,
34	#[serde(rename = "unsupported_grant_type")]
35	UnsupportedGrantType,
36	#[serde(rename = "invalid_scope")]
37	InvalidScope,
38}
39
40impl Display for AuthError {
41	fn fmt(
42		&self,
43		f: &mut std::fmt::Formatter<'_>,
44	) -> std::fmt::Result {
45		write!(f, "{:?}", self)
46	}
47}
48
49/// <https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens>
50///
51/// In order to eliminate the security related isssue,
52/// this library does not require plain private key.
53/// It is your responsibility to create `openssl::ec::EcKey`.
54/// Please see its document. In most case, you only need to use
55/// `openssl::ec::EcKey::private_key_from_pem` and `PKey::from_ec_key`.
56/// See the following code. Or just use [create_pkey][crate::auth::utils::create_pkey].
57///
58/// ```ignore
59/// let pkey = openssl::pkey::PKey::from_ec_key(
60/// 	openssl::ec::EcKey::private_key_from_pem(b"your private auth key").unwrap(),
61/// ).unwrap();
62/// ```
63#[derive(Debug, Deserialize, Serialize, Clone)]
64pub struct AppleClientSecretPayload {
65	iss: String,
66	iat: u64,
67	/// must not be greater than 15777000 (6 months in seconds) from current
68	exp: u64,
69	aud: String,
70	sub: String,
71	#[serde(skip)]
72	key_id: String,
73	#[serde(skip)]
74	pkey: Option<openssl::pkey::PKey<openssl::pkey::Private>>,
75}
76
77impl AppleClientSecretPayload {
78	/// - team_id: From developers account
79	/// - client_id: Your bundle id or web client id
80	/// - issued_at: iat. Default now.
81	/// - valid_while: Max 6 months.
82	/// - key_id: From your pem generation
83	/// - pkey: From your pem
84	pub fn new(
85		team_id: String,
86		client_id: String,
87		issued_at: Option<u64>,
88		valid_while: Option<Duration>,
89		key_id: String,
90		pkey: Option<openssl::pkey::PKey<openssl::pkey::Private>>,
91	) -> Result<Self> {
92		let issued_at = issued_at.unwrap_or(
93			SystemTime::now()
94				.duration_since(SystemTime::UNIX_EPOCH)
95				.map_err(|_| Error::SystemTime)?
96				.as_secs(),
97		);
98
99		let duration = match valid_while {
100			Some(duration) => duration,
101			_ => CLIENT_SECRET_VALID_DURATION_MAX,
102		}
103		.as_secs();
104
105		let exp = issued_at + duration;
106		Ok(Self {
107			aud: APPLE_ISSUER.to_owned(),
108			exp,
109			iat: issued_at,
110			iss: team_id,
111			sub: client_id,
112			key_id,
113			pkey,
114		})
115	}
116
117	/// Encode client secret into JWT string
118	pub fn encode(&self) -> Result<String> {
119		let pkey = match &self.pkey {
120			Some(pkey) => pkey,
121			_ => {
122				return Err(Error::SignWithKey);
123			}
124		};
125
126		let pkey = PKeyWithDigest {
127			digest: MessageDigest::sha256(),
128			key: pkey.clone(),
129		};
130
131		let header = jwt::Header {
132			algorithm: jwt::AlgorithmType::Es256,
133			key_id: Some(self.key_id.clone()),
134			..Default::default()
135		};
136
137		let client_secret = Token::new(header, self)
138			.sign_with_key(&pkey)
139			.map_err(|_| Error::SignWithKey)?
140			.as_str()
141			.to_owned();
142
143		Ok(client_secret)
144	}
145}
146
147impl TryFrom<AppleClientSecretPayload> for String {
148	type Error = Error;
149
150	fn try_from(value: AppleClientSecretPayload) -> Result<Self> {
151		value.encode()
152	}
153}