use std::{
fmt::{Debug, Display, Formatter, Result as FmtResult},
marker::PhantomData,
str::FromStr,
};
use asknothingx2_util::api::IntoRequestBuilder;
use reqwest::Client;
use crate::{
csrf::{self, CsrfConfig},
error,
request::{
ClientCredentialsRequest, ExchangeCodeRequest, RefreshRequest, RevokeRequest,
ValidateRequest,
},
types::GrantType,
AccessToken, AuthUrl, AuthorizationCode, AuthrozationRequest, ClientId, ClientSecret, Error,
RedirectUrl, RefreshToken, RevocationUrl, TokenUrl, ValidateUrl,
};
pub const AUTH_URL: &str = "https://id.twitch.tv/oauth2/authorize";
pub const TOKEN_URL: &str = "https://id.twitch.tv/oauth2/token";
pub const REVOKE_URL: &str = "https://id.twitch.tv/oauth2/revoke";
pub const VALIDATE_URL: &str = "https://id.twitch.tv/oauth2/validate";
mod private {
pub trait Sealed {}
}
pub trait OauthFlow: private::Sealed + Debug + Clone + Copy {
type RedirectUrl: Debug;
}
#[derive(Debug, Clone, Copy)]
pub struct AppAuth;
impl private::Sealed for AppAuth {}
impl OauthFlow for AppAuth {
type RedirectUrl = ();
}
#[derive(Debug, Clone, Copy)]
pub struct UserAuth;
impl private::Sealed for UserAuth {}
impl OauthFlow for UserAuth {
type RedirectUrl = RedirectUrl;
}
#[derive(Clone)]
pub struct TwitchOauth<Flow = AppAuth>
where
Flow: OauthFlow,
{
client_id: ClientId,
client_secret: ClientSecret,
redirect_uri: Flow::RedirectUrl,
secret_key: [u8; 32],
client: Client,
token_url: TokenUrl,
auth_url: AuthUrl,
revoke_url: RevocationUrl,
validate_url: ValidateUrl,
csrf_config: CsrfConfig,
phanthom: PhantomData<Flow>,
}
impl<Flow> TwitchOauth<Flow>
where
Flow: OauthFlow,
{
pub fn client_id(&self) -> &ClientId {
&self.client_id
}
#[deprecated(note = "accessing client_secret is discouraged for security reasons")]
pub fn client_secret(&self) -> &ClientSecret {
&self.client_secret
}
pub fn with_client(mut self, client: Client) -> Self {
self.client = client;
self
}
pub fn with_csrf_config(mut self, config: CsrfConfig) -> Self {
self.csrf_config = config;
self
}
pub fn with_auth_url(mut self, auth_url: AuthUrl) -> Self {
self.auth_url = auth_url;
self
}
pub fn with_token_url(mut self, token_url: TokenUrl) -> Self {
self.token_url = token_url;
self
}
pub fn with_revoke_url(mut self, revoke_url: RevocationUrl) -> Self {
self.revoke_url = revoke_url;
self
}
pub fn with_validate_url(mut self, validate_url: ValidateUrl) -> Self {
self.validate_url = validate_url;
self
}
pub fn set_client_secret(&mut self, client_secret: ClientSecret) {
self.client_secret = client_secret;
}
pub fn set_csrf_config(&mut self, config: CsrfConfig) {
self.csrf_config = config;
}
pub async fn send<T>(&self, request: T) -> Result<reqwest::Response, T::Error>
where
T: IntoRequestBuilder<Error = Error>,
{
let resp = request
.into_request_builder(&self.client)?
.send()
.await
.map_err(error::network::request)?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let v = resp.bytes().await?;
let body = String::from_utf8_lossy(&v).to_string();
return Err(error::oauth::http_error(status, body));
}
Ok(resp)
}
pub async fn refresh_access_token(
&self,
refresh_token: RefreshToken,
) -> Result<crate::UserToken, Error> {
let resp = self
.send(RefreshRequest::new(
&self.client_id,
&self.client_secret,
refresh_token,
&self.token_url,
))
.await?;
decode_response(resp).await
}
pub async fn revoke_access_token(&self, access_token: &AccessToken) -> Result<(), Error> {
let _resp = self
.send(RevokeRequest::new(
access_token,
&self.client_id,
&self.revoke_url,
))
.await?;
Ok(())
}
pub async fn app_access_token(&self) -> Result<crate::AppToken, Error> {
let resp = self
.send(ClientCredentialsRequest::new(
&self.client_id,
&self.client_secret,
GrantType::ClientCredentials,
&self.token_url,
))
.await?;
decode_response(resp).await
}
pub async fn validate_access_token(
&self,
access_token: &AccessToken,
) -> Result<crate::TokenInfo, Error> {
let resp = self
.send(ValidateRequest::new(access_token, &self.validate_url))
.await?;
decode_response(resp).await
}
}
impl TwitchOauth<AppAuth> {
pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
Self {
client_id: ClientId::from(client_id.into()),
client_secret: ClientSecret::from(client_secret.into()),
redirect_uri: (),
secret_key: csrf::generate_secret_key(),
token_url: TokenUrl::from_str(TOKEN_URL).unwrap(),
auth_url: AuthUrl::from_str(AUTH_URL).unwrap(),
revoke_url: RevocationUrl::from_str(REVOKE_URL).unwrap(),
validate_url: ValidateUrl::from_str(VALIDATE_URL).unwrap(),
client: crate::client::get().clone(),
csrf_config: CsrfConfig::default(),
phanthom: PhantomData,
}
}
pub fn with_redirect_uri(self, redirect_uri: RedirectUrl) -> TwitchOauth<UserAuth> {
TwitchOauth {
client_id: self.client_id,
client_secret: self.client_secret,
redirect_uri,
secret_key: self.secret_key,
token_url: self.token_url,
auth_url: self.auth_url,
revoke_url: self.revoke_url,
validate_url: self.validate_url,
client: self.client,
csrf_config: self.csrf_config,
phanthom: PhantomData,
}
}
pub fn from_credentials(client_id: ClientId, client_secret: ClientSecret) -> Self {
Self {
client_id,
client_secret,
redirect_uri: (),
secret_key: csrf::generate_secret_key(),
client: crate::client::get().clone(),
token_url: TokenUrl::from_str(TOKEN_URL).unwrap(),
auth_url: AuthUrl::from_str(AUTH_URL).unwrap(),
revoke_url: RevocationUrl::from_str(REVOKE_URL).unwrap(),
validate_url: ValidateUrl::from_str(VALIDATE_URL).unwrap(),
csrf_config: CsrfConfig::default(),
phanthom: PhantomData,
}
}
}
impl TwitchOauth<UserAuth> {
pub fn get_redirect_uri(&self) -> &RedirectUrl {
&self.redirect_uri
}
pub fn authorization_url<'a>(&'a self) -> AuthrozationRequest<'a> {
AuthrozationRequest::new(
&self.auth_url,
&self.client_id,
&self.redirect_uri,
csrf::generate(&self.secret_key, Some(&self.client_id)),
)
}
pub async fn exchange_code(
&self,
code: AuthorizationCode,
state: String,
) -> Result<crate::UserToken, Error> {
if csrf::verify_with_config(
&self.secret_key,
&state,
Some(&self.client_id),
&self.csrf_config,
)
.is_err()
{
return Err(error::oauth::csrf_token_mismatch());
}
let resp = self
.send(ExchangeCodeRequest::new(
&self.client_id,
&self.client_secret,
code,
&self.redirect_uri,
&self.token_url,
))
.await?;
decode_response(resp).await
}
pub fn with_secret_key(mut self, secret_key: [u8; 32]) -> Self {
self.secret_key = secret_key;
self
}
pub fn set_secret_key(&mut self, secret_key: [u8; 32]) {
self.secret_key = secret_key;
}
}
#[cfg(feature = "test")]
impl<Flow> TwitchOauth<Flow>
where
Flow: OauthFlow,
{
pub fn with_test(self) -> crate::test_oauth::TwitchOauthTest<Flow> {
crate::test_oauth::TwitchOauthTest::new(self)
}
}
impl Display for TwitchOauth<AppAuth> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "TwitchOauth(client_id: {})", self.client_id)
}
}
impl Display for TwitchOauth<UserAuth> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"TwitchOauth(client_id: {}, redirect_uri: {})",
self.client_id, self.redirect_uri
)
}
}
impl<Flow> Debug for TwitchOauth<Flow>
where
Flow: OauthFlow,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("TwitchOauth")
.field("client_id", &self.client_id)
.field("client_secret", &self.client_secret)
.field("redirect_uri", &self.redirect_uri)
.field("token_url", &self.token_url)
.field("auth_url", &self.auth_url)
.field("revoke_url", &self.revoke_url)
.finish()
}
}
pub(crate) async fn decode_response<T>(resp: reqwest::Response) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let v = resp.bytes().await?;
serde_json::from_slice(&v).map_err(|e| error::response::decode(e, String::from_utf8_lossy(&v)))
}