better_auth_api/plugins/oauth/providers/
twitch.rs1use 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#[derive(Clone, Default)]
9pub struct TwitchOptions {
10 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;