Skip to main content

better_auth_api/plugins/oauth/providers/
twitch.rs

1use better_auth_core::{AuthError, AuthResult, SchemaValue};
2use serde_json::{Map, Value, json};
3
4use super::{OAuthProvider, OAuthUserInfo, OAuthUserInfoRequest, defaults::ProviderKind};
5use crate::plugins::oauth::generic_profile::decode_claims;
6
7/// Claims requested in Twitch ID tokens.
8#[derive(Clone, Default)]
9pub struct TwitchOptions {
10    /// Use Twitch's default claims with `None`, or replace the requested claim list.
11    /// An empty list still requests email and email verification through the shared helper.
12    pub claims: Option<Vec<String>>,
13}
14
15impl TwitchOptions {
16    pub(in crate::plugins::oauth) fn authorization_claims(&self) -> AuthResult<String> {
17        let mut claims = Map::from_iter([
18            ("email".into(), Value::Null),
19            ("email_verified".into(), Value::Null),
20        ]);
21        if let Some(requested) = &self.claims {
22            claims.extend(requested.iter().map(|name| (name.clone(), Value::Null)));
23        } else {
24            claims.extend([
25                ("preferred_username".into(), Value::Null),
26                ("picture".into(), Value::Null),
27            ]);
28        }
29        better_auth_core::utils::json::stringify(&json!({"id_token": claims})).map_err(|error| {
30            AuthError::internal(format!("Failed to encode Twitch claims: {error}"))
31        })
32    }
33}
34
35impl OAuthProvider {
36    pub(in crate::plugins::oauth) fn twitch_options(&self) -> Option<&TwitchOptions> {
37        match &self.kind {
38            ProviderKind::Twitch(options) => Some(options),
39            _ => None,
40        }
41    }
42}
43
44pub(in crate::plugins::oauth) fn fetch_profile(
45    request: &OAuthUserInfoRequest,
46) -> AuthResult<Option<Value>> {
47    let Some(token) = request
48        .id_token
49        .as_deref()
50        .filter(|token| !token.is_empty())
51    else {
52        better_auth_core::observability::logger::current().error("No idToken found in token", &[]);
53        return Ok(None);
54    };
55    decode_claims(token)
56        .map(Some)
57        .ok_or_else(|| AuthError::internal("Invalid Twitch ID-token claims"))
58}
59
60pub(super) fn decode_profile(profile: Value) -> Result<OAuthUserInfo, String> {
61    Ok(OAuthUserInfo {
62        additional_fields: Default::default(),
63        id: profile
64            .get("sub")
65            .and_then(Value::as_str)
66            .ok_or("missing Twitch subject")?
67            .into(),
68        name: super::decode_profile_name(profile.get("preferred_username")),
69        email: super::defaults::profile_email(&profile)?,
70        image: profile
71            .get("picture")
72            .cloned()
73            .map(serde_json::from_value)
74            .transpose()
75            .map_err(|error| format!("Invalid Twitch picture: {error}"))?,
76        email_verified: SchemaValue::from_json(profile.get("email_verified").cloned())
77            .map_err(|error| error.to_string())?,
78    })
79}
80
81#[cfg(test)]
82#[path = "twitch_tests.rs"]
83mod tests;