Skip to main content

better_auth_api/plugins/oauth/providers/
paybin.rs

1use better_auth_core::{AuthError, AuthResult};
2use reqwest::header::HeaderMap;
3use serde_json::{Value, json};
4use url::Url;
5
6use super::{OAuthProvider, OAuthUserInfo, defaults::ProviderKind};
7use crate::plugins::oauth::oidc::{OidcVerifier, fetch_discovery};
8
9const DEFAULT_ISSUER: &str = "https://idp.paybin.io";
10
11impl OAuthProvider {
12    /// Configure Paybin with its default issuer and verified ID-token profiles.
13    pub fn paybin(client_id: &str, client_secret: &str) -> Self {
14        Self::paybin_with_issuer(client_id, client_secret, DEFAULT_ISSUER)
15    }
16
17    /// Configure a Paybin issuer while retaining the Social provider protocol.
18    /// Custom issuers declare their JWKS through OpenID discovery.
19    pub fn paybin_with_issuer(client_id: &str, client_secret: &str, issuer: &str) -> Self {
20        let issuer = if issuer.is_empty() {
21            DEFAULT_ISSUER
22        } else {
23            issuer
24        };
25        Self {
26            kind: ProviderKind::Paybin {
27                issuer: issuer.into(),
28            },
29            ..Self::custom(
30                client_id,
31                client_secret,
32                &format!("{issuer}/oauth2/authorize"),
33                &format!("{issuer}/oauth2/token"),
34            )
35        }
36    }
37
38    pub(in crate::plugins::oauth) fn paybin_issuer(&self) -> Option<&str> {
39        match &self.kind {
40            ProviderKind::Paybin { issuer } => Some(issuer),
41            _ => None,
42        }
43    }
44}
45
46pub(in crate::plugins::oauth) async fn verify(
47    issuer: &str,
48    client_id: &str,
49    token: &str,
50    nonce: Option<&str>,
51) -> AuthResult<Value> {
52    let jwks_url = if issuer == DEFAULT_ISSUER {
53        Url::parse("https://idp.paybin.io/.well-known/jwks.json")
54            .map_err(|error| AuthError::config(error.to_string()))?
55    } else {
56        let discovery_url = format!(
57            "{}/.well-known/openid-configuration",
58            issuer.trim_end_matches('/')
59        );
60        let document = fetch_discovery(&discovery_url, &HeaderMap::new())
61            .await
62            .map_err(|error| AuthError::internal(error.to_string()))?;
63        if document.issuer.as_deref() != Some(issuer) {
64            return Err(AuthError::config(
65                "Paybin discovery must describe the configured issuer",
66            ));
67        }
68        let jwks = document
69            .jwks_uri
70            .filter(|value| !value.is_empty())
71            .ok_or_else(|| AuthError::config("Paybin discovery must declare its JWKS URI"))?;
72        Url::parse(&discovery_url)
73            .and_then(|url| url.join(&jwks))
74            .map_err(|error| AuthError::config(error.to_string()))?
75    };
76    // Discovery locates keys; application configuration still fixes issuer and audience.
77    let verifier = OidcVerifier::new(
78        jwks_url,
79        issuer.into(),
80        client_id.into(),
81        Some(vec![json!("RS256")]),
82    )
83    .map_err(|error| AuthError::internal(error.to_string()))?;
84    verifier
85        .verify(token, nonce)
86        .await
87        .map_err(|_| crate::plugins::oauth::id_token::invalid())
88}
89
90pub(super) fn decode_profile(profile: Value) -> Result<OAuthUserInfo, String> {
91    let name = ["name", "preferred_username"]
92        .into_iter()
93        .find_map(|key| {
94            profile
95                .get(key)
96                .and_then(Value::as_str)
97                .filter(|value| !value.is_empty())
98        })
99        .unwrap_or_default()
100        .to_owned();
101    let verified = super::profile_email_verified(
102        &better_auth_core::SchemaValue::from_json(profile.get("email_verified").cloned())
103            .map_err(|error| error.to_string())?,
104    )
105    .map_err(|error| error.to_string())?;
106    Ok(OAuthUserInfo {
107        id: profile
108            .get("sub")
109            .and_then(Value::as_str)
110            .ok_or("missing sub")?
111            .into(),
112        email: super::defaults::profile_email(&profile)?,
113        name: Some(name).into(),
114        image: profile
115            .get("picture")
116            .cloned()
117            .map(serde_json::from_value)
118            .transpose()
119            .map_err(|error| format!("Invalid Paybin picture: {error}"))?,
120        email_verified: Some(verified).into(),
121        additional_fields: Default::default(),
122    })
123}