1use oauth2::{
2 AuthUrl, ClientId, ClientSecret, CsrfToken,
3 PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, TokenUrl,
4 basic::BasicClient,
5};
6use serde::Deserialize;
7use std::env;
8
9#[derive(Debug, Clone, Deserialize)]
13pub struct OAuthTokenResponse {
14 pub access_token: String,
16
17 pub refresh_token: Option<String>,
19
20 pub expires_in: u64,
22
23 pub scope: String,
25
26 pub token_type: String,
28}
29
30pub struct KickOAuth {
32 client: BasicClient,
33}
34
35impl KickOAuth {
36 pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
43 let client_id = env::var("KICK_CLIENT_ID")?;
45 let client_secret = env::var("KICK_CLIENT_SECRET")?;
46 let redirect_uri = env::var("KICK_REDIRECT_URI")?;
47
48 if client_id.is_empty() || client_secret.is_empty() || redirect_uri.is_empty() {
50 return Err("One or more OAuth credentials are empty!".into());
51 }
52
53 let auth_url = AuthUrl::new("https://id.kick.com/oauth/authorize".to_string())?;
55 let token_url = TokenUrl::new("https://id.kick.com/oauth/token".to_string())?;
56
57 let client = BasicClient::new(
59 ClientId::new(client_id),
60 Some(ClientSecret::new(client_secret)),
61 auth_url,
62 Some(token_url),
63 )
64 .set_redirect_uri(RedirectUrl::new(redirect_uri)?);
65
66 Ok(Self { client })
67 }
68
69 pub fn get_authorization_url(&self, scopes: Vec<&str>) -> (String, CsrfToken, PkceCodeVerifier) {
78 let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
80
81 let mut auth_request = self.client
82 .authorize_url(CsrfToken::new_random)
83 .set_pkce_challenge(pkce_challenge);
84
85 for scope in scopes {
87 auth_request = auth_request.add_scope(Scope::new(scope.to_string()));
88 }
89
90 let (auth_url, csrf_token) = auth_request.url();
91
92 (auth_url.to_string(), csrf_token, pkce_verifier)
93 }
94
95 pub async fn exchange_code(
102 &self,
103 code: String,
104 pkce_verifier: PkceCodeVerifier,
105 ) -> Result<OAuthTokenResponse, Box<dyn std::error::Error>> {
106 let client_id = env::var("KICK_CLIENT_ID")?;
107 let client_secret = env::var("KICK_CLIENT_SECRET")?;
108 let redirect_uri = env::var("KICK_REDIRECT_URI")?;
109
110 let http_client = reqwest::Client::new();
111 let response = http_client
112 .post("https://id.kick.com/oauth/token")
113 .form(&[
114 ("grant_type", "authorization_code"),
115 ("code", &code),
116 ("client_id", &client_id),
117 ("client_secret", &client_secret),
118 ("redirect_uri", &redirect_uri),
119 ("code_verifier", pkce_verifier.secret()),
120 ])
121 .send()
122 .await?;
123
124 let status = response.status();
125 let body = response.text().await?;
126
127 if status.is_success() {
128 let token_response: OAuthTokenResponse = serde_json::from_str(&body)?;
129 Ok(token_response)
130 } else {
131 Err(format!("Token exchange failed: {}", body).into())
132 }
133 }
134
135 pub async fn refresh_token(
143 &self,
144 refresh_token: &str,
145 ) -> Result<OAuthTokenResponse, Box<dyn std::error::Error>> {
146 let client_id = env::var("KICK_CLIENT_ID")?;
147 let client_secret = env::var("KICK_CLIENT_SECRET")?;
148
149 let http_client = reqwest::Client::new();
150 let response = http_client
151 .post("https://id.kick.com/oauth/token")
152 .form(&[
153 ("grant_type", "refresh_token"),
154 ("refresh_token", refresh_token),
155 ("client_id", &client_id),
156 ("client_secret", &client_secret),
157 ])
158 .send()
159 .await?;
160
161 let status = response.status();
162 let body = response.text().await?;
163
164 if status.is_success() {
165 let token_response: OAuthTokenResponse = serde_json::from_str(&body)?;
166 Ok(token_response)
167 } else {
168 Err(format!("Token refresh failed: {}", body).into())
169 }
170 }
171
172 pub async fn revoke_token(
179 &self,
180 token: &str,
181 ) -> Result<(), Box<dyn std::error::Error>> {
182 let client_id = env::var("KICK_CLIENT_ID")?;
183 let client_secret = env::var("KICK_CLIENT_SECRET")?;
184
185 let http_client = reqwest::Client::new();
186 let response = http_client
187 .post("https://id.kick.com/oauth/revoke")
188 .form(&[
189 ("token", token),
190 ("client_id", &client_id),
191 ("client_secret", &client_secret),
192 ])
193 .send()
194 .await?;
195
196 let status = response.status();
197 if status.is_success() {
198 Ok(())
199 } else {
200 let body = response.text().await?;
201 Err(format!("Token revocation failed: {}", body).into())
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_oauth_from_env() {
212 dotenvy::dotenv().ok();
215
216 match KickOAuth::from_env() {
217 Ok(oauth) => {
218 let scopes = vec!["user:read", "channel:read"];
219 let (url, _csrf, _verifier) = oauth.get_authorization_url(scopes);
220 println!("Auth URL: {}", url);
221 assert!(url.contains("kick.com"));
222 assert!(url.contains("code_challenge")); }
224 Err(e) => {
225 println!("Expected failure (env vars not set): {}", e);
226 }
227 }
228 }
229}