better_auth_api/plugins/oauth/providers/
paypal.rs1use better_auth_core::{AuthError, AuthResult, SchemaValue};
2use serde_json::Value;
3
4use super::{OAuthProvider, OAuthUserInfo, OAuthUserInfoRequest, OAuthUserInfoResponse};
5use crate::plugins::oauth::google::AcceptedIdToken;
6use crate::plugins::oauth::social_profile::{apply_mapped_profile, fetch_http_profile};
7
8impl OAuthProvider {
9 pub fn paypal(client_id: &str, client_secret: &str) -> Self {
12 Self {
13 kind: super::ProviderKind::PayPal,
14 user_info_url: Some(
15 "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo".into(),
16 ),
17 ..Self::custom(
18 client_id,
19 client_secret,
20 "https://www.sandbox.paypal.com/signin/authorize",
21 "https://api-m.sandbox.paypal.com/v1/oauth2/token",
22 )
23 }
24 }
25
26 pub fn paypal_live(client_id: &str, client_secret: &str) -> Self {
28 Self {
29 auth_url: "https://www.paypal.com/signin/authorize".into(),
30 token_url: "https://api-m.paypal.com/v1/oauth2/token".into(),
31 user_info_url: Some("https://api-m.paypal.com/v1/identity/oauth2/userinfo".into()),
32 ..Self::paypal(client_id, client_secret)
33 }
34 }
35
36 pub(in crate::plugins::oauth) fn is_paypal(&self) -> bool {
37 matches!(self.kind, super::ProviderKind::PayPal)
38 }
39}
40
41pub(super) fn decode_profile(profile: Value) -> Result<OAuthUserInfo, String> {
42 Ok(OAuthUserInfo {
43 id: profile
44 .get("user_id")
45 .and_then(Value::as_str)
46 .ok_or("Missing PayPal user_id")?
47 .into(),
48 name: profile
49 .get("name")
50 .cloned()
51 .map(serde_json::from_value::<Option<String>>)
52 .transpose()
53 .map_err(|error| format!("Invalid PayPal name: {error}"))?
54 .map(Into::into)
55 .unwrap_or_default(),
56 email: super::defaults::profile_email(&profile)?,
57 image: profile
58 .get("picture")
59 .cloned()
60 .map(serde_json::from_value)
61 .transpose()
62 .map_err(|error| format!("Invalid PayPal picture: {error}"))?,
63 email_verified: SchemaValue::from_json(profile.get("email_verified").cloned())
64 .map_err(|error| error.to_string())?,
65 additional_fields: Default::default(),
66 })
67}
68
69pub(in crate::plugins::oauth) async fn fetch_user_info(
70 provider: &OAuthProvider,
71 request: OAuthUserInfoRequest,
72 expected_nonce: Option<&str>,
73 claims: Option<Value>,
74) -> AuthResult<Option<OAuthUserInfoResponse>> {
75 let logger = better_auth_core::observability::logger::current();
76 let Some(access_token) = request
77 .access_token
78 .as_deref()
79 .filter(|token| !token.is_empty())
80 else {
81 logger.error("Access token is required to fetch PayPal user info", &[]);
82 return Ok(None);
83 };
84 let result: AuthResult<Option<OAuthUserInfoResponse>> = async {
85 let url = provider
86 .user_info_url
87 .as_deref()
88 .ok_or_else(|| AuthError::internal("Missing PayPal user_info_url"))?;
89 let profile = fetch_http_profile(
90 provider
91 .user_info_request(url, access_token)
92 .query(&[("schema", "paypalv1.1")]),
93 )
94 .await?;
95 let Some(profile) = profile.filter(|profile| !profile.is_null()) else {
96 logger.error("Failed to fetch user info from PayPal", &[]);
97 return Ok(None);
98 };
99 if let Some(token) = request
100 .id_token
101 .as_deref()
102 .filter(|token| !token.is_empty())
103 {
104 let claims = match claims {
105 Some(claims) => claims,
106 None => {
107 let verifier = provider.verify_id_token.as_deref().ok_or_else(|| {
108 AuthError::internal(
109 "PayPal ID token requires an explicit application verifier",
110 )
111 })?;
112 AcceptedIdToken::verify(verifier, token, expected_nonce, None)
113 .await
114 .ok_or_else(crate::plugins::oauth::id_token::invalid)?
115 .value()?
116 }
117 };
118 let subject = claims
119 .get("sub")
120 .and_then(Value::as_str)
121 .filter(|subject| !subject.is_empty());
122 let profile_subject = profile
123 .get("sub")
124 .filter(|value| !value.is_null())
125 .or_else(|| profile.get("user_id"))
126 .and_then(Value::as_str);
127 if subject.is_none() || subject != profile_subject {
128 logger.error(
129 "PayPal user info subject does not match ID token subject",
130 &[],
131 );
132 return Ok(None);
133 }
134 }
135 let Some(user) = provider.decode_profile(profile.clone())? else {
136 return Ok(None);
137 };
138 let mut response = OAuthUserInfoResponse {
139 user,
140 data: profile,
141 };
142 if let Some(mapper) = &provider.map_profile_to_user {
143 let mapped = mapper.map_profile(&response.data).await?;
144 apply_mapped_profile(&mut response, mapped);
145 }
146 let _ = response.user.email()?;
147 let _ = response.user.email_verified()?;
148 Ok(Some(response))
149 }
150 .await;
151 match result {
152 Ok(response) => Ok(response),
153 Err(error) => {
154 logger.error(
155 "Failed to fetch user info from PayPal:",
156 &[better_auth_core::observability::LogArgument::Error(&error)],
157 );
158 Ok(None)
159 }
160 }
161}
162
163pub(in crate::plugins::oauth) fn token_result(
164 result: AuthResult<super::OAuthTokenSet>,
165 grant: crate::plugins::oauth::token::TokenGrantType,
166) -> AuthResult<super::OAuthTokenSet> {
167 use crate::plugins::oauth::token::TokenGrantType;
168 result.map_err(|error| {
169 let (message, code) = match grant {
170 TokenGrantType::AuthorizationCode => (
171 "PayPal token exchange failed:",
172 "FAILED_TO_GET_ACCESS_TOKEN",
173 ),
174 TokenGrantType::RefreshToken => (
175 "PayPal token refresh failed:",
176 "FAILED_TO_REFRESH_ACCESS_TOKEN",
177 ),
178 };
179 better_auth_core::observability::logger::current().error(
180 message,
181 &[better_auth_core::observability::LogArgument::Error(&error)],
182 );
183 AuthError::internal(code)
184 })
185}