siwa-async 0.5.1

Sign In With Apple Validation in Async Rust
Documentation
//! Common Object
use std::{
	fmt::Display,
	time::{Duration, SystemTime},
};

use jwt::{PKeyWithDigest, SignWithKey, Token};
use openssl::hash::MessageDigest;
use serde::{self, Deserialize, Serialize};
use thiserror::Error;

use crate::error::{Error, Result};

use super::constants::{
	APPLE_ISSUER, CLIENT_SECRET_VALID_DURATION_MAX,
};

/// <https://developer.apple.com/documentation/sign_in_with_apple/errorresponse>
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct AuthErrorResponse {
	pub error: AuthError,
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Error)]
pub enum AuthError {
	#[serde(rename = "invalid_request")]
	InvalidRequest,
	#[serde(rename = "invalid_client")]
	InvalidClient,
	#[serde(rename = "invalid_grant")]
	InvalidGrant,
	#[serde(rename = "unauthorized_client")]
	UnauthorizedClient,
	#[serde(rename = "unsupported_grant_type")]
	UnsupportedGrantType,
	#[serde(rename = "invalid_scope")]
	InvalidScope,
}

impl Display for AuthError {
	fn fmt(
		&self,
		f: &mut std::fmt::Formatter<'_>,
	) -> std::fmt::Result {
		write!(f, "{:?}", self)
	}
}

/// <https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens>
///
/// In order to eliminate the security related isssue,
/// this library does not require plain private key.
/// It is your responsibility to create `openssl::ec::EcKey`.
/// Please see its document. In most case, you only need to use
/// `openssl::ec::EcKey::private_key_from_pem` and `PKey::from_ec_key`.
/// See the following code. Or just use [create_pkey][crate::auth::utils::create_pkey].
///
/// ```ignore
/// let pkey = openssl::pkey::PKey::from_ec_key(
/// 	openssl::ec::EcKey::private_key_from_pem(b"your private auth key").unwrap(),
/// ).unwrap();
/// ```
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AppleClientSecretPayload {
	iss: String,
	iat: u64,
	/// must not be greater than 15777000 (6 months in seconds) from current
	exp: u64,
	aud: String,
	sub: String,
	#[serde(skip)]
	key_id: String,
	#[serde(skip)]
	pkey: Option<openssl::pkey::PKey<openssl::pkey::Private>>,
}

impl AppleClientSecretPayload {
	/// - team_id: From developers account
	/// - client_id: Your bundle id or web client id
	/// - issued_at: iat. Default now.
	/// - valid_while: Max 6 months.
	/// - key_id: From your pem generation
	/// - pkey: From your pem
	pub fn new(
		team_id: String,
		client_id: String,
		issued_at: Option<u64>,
		valid_while: Option<Duration>,
		key_id: String,
		pkey: Option<openssl::pkey::PKey<openssl::pkey::Private>>,
	) -> Result<Self> {
		let issued_at = issued_at.unwrap_or(
			SystemTime::now()
				.duration_since(SystemTime::UNIX_EPOCH)
				.map_err(|_| Error::SystemTime)?
				.as_secs(),
		);

		let duration = match valid_while {
			Some(duration) => duration,
			_ => CLIENT_SECRET_VALID_DURATION_MAX,
		}
		.as_secs();

		let exp = issued_at + duration;
		Ok(Self {
			aud: APPLE_ISSUER.to_owned(),
			exp,
			iat: issued_at,
			iss: team_id,
			sub: client_id,
			key_id,
			pkey,
		})
	}

	/// Encode client secret into JWT string
	pub fn encode(&self) -> Result<String> {
		let pkey = match &self.pkey {
			Some(pkey) => pkey,
			_ => {
				return Err(Error::SignWithKey);
			}
		};

		let pkey = PKeyWithDigest {
			digest: MessageDigest::sha256(),
			key: pkey.clone(),
		};

		let header = jwt::Header {
			algorithm: jwt::AlgorithmType::Es256,
			key_id: Some(self.key_id.clone()),
			..Default::default()
		};

		let client_secret = Token::new(header, self)
			.sign_with_key(&pkey)
			.map_err(|_| Error::SignWithKey)?
			.as_str()
			.to_owned();

		Ok(client_secret)
	}
}

impl TryFrom<AppleClientSecretPayload> for String {
	type Error = Error;

	fn try_from(value: AppleClientSecretPayload) -> Result<Self> {
		value.encode()
	}
}