siwa-async 0.5.1

Sign In With Apple Validation in Async Rust
Documentation
//! Generate and Validate Tokens
use hyper::StatusCode;
use serde::{self, Deserialize, Serialize};

use crate::{
	common::constants::{
		APPLE_AUTH_TOKEN_ENDPOINT, APPLE_REVOKE_TOKENS_ENDPOINT,
	},
	error::{Error, Result},
	object::{AppleClientSecretPayload, AuthError},
};

/// <https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens>
/// <https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens>
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub struct AuthTokenRequest {
	/// Bundle id
	client_id: String,
	/// From [AppleClientSecretPayload][crate::object::AppleClientSecretPayload]
	client_secret: String,
	/// Authorization code
	code: Option<String>,
	/// [AuthGrantType]
	grant_type: AuthGrantType,
	/// Refresh token
	refresh_token: Option<String>,
	/// Redirect uri for web or when you provided it when SIWA
	redirect_uri: Option<String>,
}

impl AuthTokenRequest {
	/// - client_id: Bundle id
	/// - client_secret: From [AppleClientSecretPayload][crate::object::AppleClientSecretPayload]
	pub fn new(
		client_id: String,
		client_secret: AppleClientSecretPayload,
		redirect_uri: Option<String>,
	) -> Result<Self> {
		Ok(Self {
			client_id,
			client_secret: client_secret.encode()?,
			redirect_uri,
			code: None,
			refresh_token: None,
			grant_type: AuthGrantType::AuthorizationCode,
		})
	}
	pub async fn validate_auth_code(
		&self,
		auth_code: &str,
	) -> Result<AuthTokenResponse> {
		let mut request = self.clone();
		request.grant_type = AuthGrantType::AuthorizationCode;
		request.code = Some(auth_code.to_owned());
		request.refresh_token = None;
		request.validate().await
	}

	pub async fn validate_refresh_token(
		&self,
		refresh_token: &str,
	) -> Result<AuthTokenResponse> {
		let mut request = self.clone();
		request.grant_type = AuthGrantType::RefreshToken;
		request.refresh_token = Some(refresh_token.to_owned());
		request.code = None;
		request.validate().await
	}

	async fn validate(&self) -> Result<AuthTokenResponse> {
		let client = reqwest::Client::new();
		let res = client
			.post(APPLE_AUTH_TOKEN_ENDPOINT)
			.form(self)
			.send()
			.await
			.map_err(|e| Error::HttpError(format!("{:?}", e)))?;
		match &res.status() {
			&StatusCode::OK => {
				Ok(res.json::<AuthTokenResponse>().await?)
			}
			_ => {
				Err(Error::BadRequest(res.json::<AuthError>().await?))
			}
		}
	}

	pub async fn revoke_refresh_token(
		&self,
		refresh_token: String,
	) -> Result<()> {
		self.revoke(refresh_token, &AuthTokenType::RefreshToken)
			.await
	}

	pub async fn revoke_access_token(
		&self,
		access_token: String,
	) -> Result<()> {
		self.revoke(access_token, &AuthTokenType::AccessToken).await
	}

	async fn revoke(
		&self,
		token: String,
		token_type: &AuthTokenType,
	) -> Result<()> {
		let client = reqwest::Client::new();
		let params = [
			("client_id", self.client_id.clone()),
			("client_secret", self.client_secret.clone()),
			("token", token),
			(
				"token_type_hint",
				serde_json::ser::to_string(token_type).unwrap(),
			),
		];
		let res = client
			.post(APPLE_REVOKE_TOKENS_ENDPOINT)
			.form(&params)
			.send()
			.await
			.map_err(|e| Error::HttpError(format!("{:?}", e)))?;

		match &res.status() {
			&StatusCode::OK => Ok(()),
			_ => Err(Error::BadRequest(
				res.json::<AuthError>().await.unwrap(),
			)),
		}
	}
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
enum AuthGrantType {
	/// "authorization_code"
	#[serde(rename = "authorization_code")]
	AuthorizationCode,

	/// "refresh_token"
	#[serde(rename = "refresh_token")]
	RefreshToken,
}

/// <https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse>
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct AuthTokenResponse {
	pub access_token: String,
	pub expires_in: u64,
	pub id_token: String,
	/// This is not present when the request is `refresh_token`
	pub refresh_token: Option<String>,
	/// Always "Bearer"
	pub token_type: String,
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub enum AuthTokenType {
	#[serde(rename = "refresh_token")]
	RefreshToken,
	#[serde(rename = "access_token")]
	AccessToken,
}