use crate::ClientSecret;
use crate::{
id::TwitchTokenErrorResponse,
tokens::{
errors::{RefreshTokenError, UserTokenExchangeError, ValidationError},
Scope, TwitchToken,
},
};
use oauth2::{AccessToken, ClientId, RedirectUrl, RefreshToken};
use oauth2::{HttpRequest, HttpResponse};
use std::future::Future;
#[derive(Debug, Clone)]
pub struct UserToken {
pub access_token: AccessToken,
client_id: ClientId,
client_secret: Option<ClientSecret>,
login: Option<String>,
pub refresh_token: Option<RefreshToken>,
expires: Option<std::time::Instant>,
scopes: Vec<Scope>,
}
impl UserToken {
pub fn from_existing_unchecked(
access_token: impl Into<AccessToken>,
refresh_token: impl Into<Option<RefreshToken>>,
client_id: impl Into<ClientId>,
client_secret: impl Into<Option<ClientSecret>>,
login: Option<String>,
scopes: Option<Vec<Scope>>,
) -> UserToken {
UserToken {
access_token: access_token.into(),
client_id: client_id.into(),
client_secret: client_secret.into(),
login,
refresh_token: refresh_token.into(),
expires: None,
scopes: scopes.unwrap_or_else(Vec::new),
}
}
pub async fn from_existing<RE, C, F>(
http_client: C,
access_token: AccessToken,
refresh_token: impl Into<Option<RefreshToken>>,
client_secret: impl Into<Option<ClientSecret>>,
) -> Result<UserToken, ValidationError<RE>>
where
RE: std::error::Error + Send + Sync + 'static,
C: FnOnce(HttpRequest) -> F,
F: Future<Output = Result<HttpResponse, RE>>,
{
let validated = crate::validate_token(http_client, &access_token).await?;
Ok(Self::from_existing_unchecked(
access_token,
refresh_token.into(),
validated.client_id,
client_secret,
validated.login,
validated.scopes,
))
}
pub fn builder(
client_id: ClientId,
client_secret: ClientSecret,
redirect_url: RedirectUrl,
) -> Result<UserTokenBuilder, oauth2::url::ParseError> {
UserTokenBuilder::new(client_id, client_secret, redirect_url)
}
}
#[async_trait::async_trait(?Send)]
impl TwitchToken for UserToken {
fn client_id(&self) -> &ClientId { &self.client_id }
fn token(&self) -> &AccessToken { &self.access_token }
fn login(&self) -> Option<&str> { self.login.as_deref() }
async fn refresh_token<RE, C, F>(
&mut self,
http_client: C,
) -> Result<(), RefreshTokenError<RE>>
where
RE: std::error::Error + Send + Sync + 'static,
C: FnOnce(HttpRequest) -> F,
F: Future<Output = Result<HttpResponse, RE>>,
{
if let Some(client_secret) = self.client_secret.clone() {
let (access_token, expires, refresh_token) = if let Some(token) =
self.refresh_token.take()
{
crate::refresh_token(http_client, token, &self.client_id, &client_secret).await?
} else {
return Err(RefreshTokenError::NoRefreshToken);
};
self.access_token = access_token;
self.expires = expires;
self.refresh_token = refresh_token;
Ok(())
} else {
return Err(RefreshTokenError::NoClientSecretFound);
}
}
fn expires(&self) -> Option<std::time::Instant> { None }
fn scopes(&self) -> Option<&[Scope]> { Some(self.scopes.as_slice()) }
}
pub struct UserTokenBuilder {
pub(crate) scopes: Vec<Scope>,
pub(crate) client: crate::TwitchClient,
pub(crate) csrf: Option<oauth2::CsrfToken>,
pub(crate) force_verify: bool,
pub(crate) redirect_url: RedirectUrl,
client_id: ClientId,
client_secret: ClientSecret,
}
impl UserTokenBuilder {
pub fn new(
client_id: ClientId,
client_secret: ClientSecret,
redirect_url: RedirectUrl,
) -> Result<UserTokenBuilder, oauth2::url::ParseError> {
Ok(UserTokenBuilder {
scopes: vec![],
client: crate::TwitchClient::new(
client_id.clone(),
Some(client_secret.clone()),
oauth2::AuthUrl::new(crate::AUTH_URL.to_string())?,
Some(oauth2::TokenUrl::new(crate::TOKEN_URL.to_string())?),
)
.set_auth_type(oauth2::AuthType::BasicAuth)
.set_redirect_uri(redirect_url.clone()),
csrf: None,
force_verify: false,
redirect_url,
client_id,
client_secret,
})
}
pub fn force_verify(mut self, b: bool) -> Self {
self.force_verify = b;
self
}
pub fn generate_url(&mut self) -> (oauth2::url::Url, oauth2::CsrfToken) {
let mut auth = self.client.authorize_url(oauth2::CsrfToken::new_random);
for scope in self.scopes.iter() {
auth = auth.add_scope(scope.as_oauth_scope())
}
auth = auth.add_extra_param(
"force_verify",
if self.force_verify { "true" } else { "false" },
);
let (url, csrf) = auth.url();
self.csrf = Some(csrf.clone());
(url, csrf)
}
#[doc(hidden)]
pub fn set_csrf(&mut self, csrf: oauth2::CsrfToken) { self.csrf = Some(csrf); }
pub async fn get_user_token<RE, C, F>(
self,
http_client: C,
state: Option<&str>,
code: oauth2::AuthorizationCode,
) -> Result<UserToken, UserTokenExchangeError<RE>>
where
RE: std::error::Error + Send + Sync + 'static,
C: Copy + FnOnce(HttpRequest) -> F,
F: Future<Output = Result<HttpResponse, RE>>,
{
if let Some(csrf) = self.csrf {
if state.is_none() || csrf.secret() != state.expect("should not fail") {
return Err(UserTokenExchangeError::StateMismatch);
}
} else {
return Err(UserTokenExchangeError::StateMismatch);
}
use oauth2::http::{HeaderMap, Method, StatusCode};
use std::collections::HashMap;
let mut params = HashMap::new();
params.insert("client_id", self.client_id.as_str());
params.insert("client_secret", self.client_secret.secret().as_str());
params.insert("code", code.secret().as_str());
params.insert("grant_type", "authorization_code");
params.insert("redirect_uri", self.redirect_url.as_str());
let req = HttpRequest {
url: oauth2::url::Url::parse_with_params(crate::TOKEN_URL, ¶ms)
.expect("unexpectedly failed to parse revoke url"),
method: Method::POST,
headers: HeaderMap::new(),
body: vec![],
};
let resp = http_client(req)
.await
.map_err(UserTokenExchangeError::RequestError)?;
match resp.status_code {
StatusCode::BAD_REQUEST => {
return Err(UserTokenExchangeError::TwitchError(
TwitchTokenErrorResponse {
status: StatusCode::BAD_REQUEST,
message: String::from_utf8_lossy(&resp.body).into_owned(),
},
))
}
StatusCode::OK => (),
_ => todo!(),
};
let response: crate::id::TwitchTokenResponse<
oauth2::EmptyExtraTokenFields,
oauth2::basic::BasicTokenType,
> = serde_json::from_slice(resp.body.as_slice())?;
UserToken::from_existing(
http_client,
response.access_token,
response.refresh_token,
None,
)
.await
.map_err(Into::into)
}
}
#[cfg(test)]
mod tests {
pub use super::*;
#[test]
fn generate_url() {
dbg!(UserTokenBuilder::new(
ClientId::new("clientid".to_string()),
ClientSecret::new("secret".to_string()),
oauth2::RedirectUrl::new("https://localhost".to_string()).unwrap(),
)
.unwrap()
.force_verify(true)
.generate_url()
.0
.to_string());
}
#[tokio::test]
#[ignore]
async fn get_token() {
let mut t = UserTokenBuilder::new(
ClientId::new("clientid".to_string()),
ClientSecret::new("secret".to_string()),
oauth2::RedirectUrl::new(r#"https://localhost"#.to_string()).unwrap(),
)
.unwrap()
.force_verify(true);
t.csrf = Some(oauth2::CsrfToken::new("random".to_string()));
let token = t
.get_user_token(
crate::client::surf_http_client,
Some("random"),
oauth2::AuthorizationCode::new("authcode".to_string()),
)
.await
.unwrap();
println!("token: {:?} - {}", token, token.access_token.secret());
}
}