mod app_access_token;
pub mod errors;
mod user_token;
pub use app_access_token::AppAccessToken;
use twitch_types::{UserId, UserIdRef, UserName, UserNameRef};
pub use user_token::{ImplicitUserTokenBuilder, UserToken, UserTokenBuilder};
#[cfg(feature = "client")]
use crate::client::Client;
use crate::{id::TwitchTokenErrorResponse, scopes::Scope, RequestParseError};
use errors::ValidationError;
#[cfg(feature = "client")]
use errors::{RefreshTokenError, RevokeTokenError};
use crate::types::{AccessToken, ClientId};
use serde_derive::Deserialize;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BearerTokenType {
UserToken,
AppAccessToken,
}
#[cfg_attr(feature = "client", async_trait::async_trait)]
pub trait TwitchToken {
fn token_type() -> BearerTokenType;
fn client_id(&self) -> &ClientId;
fn token(&self) -> &AccessToken;
fn login(&self) -> Option<&UserNameRef>;
fn user_id(&self) -> Option<&UserIdRef>;
#[cfg(feature = "client")]
async fn refresh_token<'a, C>(
&mut self,
http_client: &'a C,
) -> Result<(), RefreshTokenError<<C as Client>::Error>>
where
Self: Sized,
C: Client;
fn expires_in(&self) -> std::time::Duration;
fn is_elapsed(&self) -> bool {
let exp = self.expires_in();
exp.as_secs() == 0 && exp.as_nanos() == 0
}
fn scopes(&self) -> &[Scope];
#[cfg(feature = "client")]
async fn validate_token<'a, C>(
&self,
http_client: &'a C,
) -> Result<ValidatedToken, ValidationError<<C as Client>::Error>>
where
Self: Sized,
C: Client,
{
let token = &self.token();
token.validate_token(http_client).await
}
#[cfg(feature = "client")]
async fn revoke_token<'a, C>(
self,
http_client: &'a C,
) -> Result<(), RevokeTokenError<<C as Client>::Error>>
where
Self: Sized,
C: Client,
{
let token = self.token();
let client_id = self.client_id();
token.revoke_token(http_client, client_id).await
}
}
#[cfg_attr(feature = "client", async_trait::async_trait)]
impl<T: TwitchToken + Send> TwitchToken for Box<T> {
fn token_type() -> BearerTokenType { T::token_type() }
fn client_id(&self) -> &ClientId { (**self).client_id() }
fn token(&self) -> &AccessToken { (**self).token() }
fn login(&self) -> Option<&UserNameRef> { (**self).login() }
fn user_id(&self) -> Option<&UserIdRef> { (**self).user_id() }
#[cfg(feature = "client")]
async fn refresh_token<'a, C>(
&mut self,
http_client: &'a C,
) -> Result<(), RefreshTokenError<<C as Client>::Error>>
where
Self: Sized,
C: Client,
{
(**self).refresh_token(http_client).await
}
fn expires_in(&self) -> std::time::Duration { (**self).expires_in() }
fn scopes(&self) -> &[Scope] { (**self).scopes() }
}
#[derive(Debug, Clone, Deserialize)]
pub struct ValidatedToken {
pub client_id: ClientId,
pub login: Option<UserName>,
pub user_id: Option<UserId>,
pub scopes: Option<Vec<Scope>>,
#[serde(deserialize_with = "expires_in")]
pub expires_in: Option<std::time::Duration>,
}
fn expires_in<'a, D: serde::de::Deserializer<'a>>(
d: D,
) -> Result<Option<std::time::Duration>, D::Error> {
use serde::Deserialize;
let num = u64::deserialize(d)?;
if num == 0 {
Ok(None)
} else {
Ok(Some(std::time::Duration::from_secs(num)))
}
}
impl ValidatedToken {
pub fn from_response<B: AsRef<[u8]>>(
response: &http::Response<B>,
) -> Result<ValidatedToken, ValidationError<std::convert::Infallible>> {
match crate::parse_response(response) {
Ok(ok) => Ok(ok),
Err(err) => match err {
RequestParseError::TwitchError(TwitchTokenErrorResponse { status, .. })
if status == http::StatusCode::UNAUTHORIZED =>
{
Err(ValidationError::NotAuthorized)
}
err => Err(err.into()),
},
}
}
}
#[cfg(test)]
mod tests {
use crate::ValidatedToken;
use super::errors::ValidationError;
#[test]
fn validated_token() {
let body = br#"
{
"client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
"login": "twitchdev",
"scopes": [
"channel:read:subscriptions"
],
"user_id": "141981764",
"expires_in": 5520838
}
"#;
let response = http::Response::builder().status(200).body(body).unwrap();
ValidatedToken::from_response(&response).unwrap();
}
#[test]
fn validated_non_expiring_token() {
let body = br#"
{
"client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
"login": "twitchdev",
"scopes": [
"channel:read:subscriptions"
],
"user_id": "141981764",
"expires_in": 0
}
"#;
let response = http::Response::builder().status(200).body(body).unwrap();
let token = ValidatedToken::from_response(&response).unwrap();
assert!(token.expires_in.is_none());
}
#[test]
fn validated_error_response() {
let body = br#"
{
"status": 401,
"message": "missing authorization token",
}
"#;
let response = http::Response::builder().status(401).body(body).unwrap();
let error = ValidatedToken::from_response(&response).unwrap_err();
assert!(matches!(error, ValidationError::RequestParseError(_)))
}
}