twitch_oauth_token 4.4.0

Type-safe Twitch OAuth 2.0 authentication library with CSRF protection and full scope support
Documentation
pub mod url {
    use twitch_oauth_token::{AuthUrl, RedirectUrl, RevocationUrl, TokenUrl, ValidateUrl};

    use std::str::FromStr;

    pub fn token(url: &str) -> TokenUrl {
        TokenUrl::from_str(&format!("{url}/oauth2/token",)).unwrap()
    }

    pub fn auth(url: &str) -> AuthUrl {
        AuthUrl::from_str(&format!("{url}/oauth2/authorize")).unwrap()
    }

    pub fn redirect() -> RedirectUrl {
        RedirectUrl::from_str("http://localhost:3000").unwrap()
    }

    pub fn revoke(url: &str) -> RevocationUrl {
        RevocationUrl::from_str(&format!("{url}/oauth2/revoke")).unwrap()
    }

    pub fn validate(url: &str) -> ValidateUrl {
        ValidateUrl::from_str(&format!("{url}/oauth2/validate")).unwrap()
    }
}

pub mod token {
    use twitch_oauth_token::{AccessToken, RefreshToken};

    pub fn refresh() -> RefreshToken {
        RefreshToken::from("eyJfaWQmNzMtNGCJ9%6VFV5LNrZFUj8oU231/3Aj")
    }

    pub fn access() -> AccessToken {
        AccessToken::from("jostpf5q0uzmxmkba9iyug38kjtgh")
    }

    pub fn code() -> String {
        "gulfwdmys5lsm6qyz4xiz9q32l10".to_string()
    }
}

#[cfg(feature = "test")]
pub mod setup {
    use twitch_oauth_token::{TwitchOauth, UserAuth};
    use wiremock::MockServer;

    use super::{config, url};

    pub async fn oauth_server() -> (MockServer, TwitchOauth<UserAuth>) {
        let server = MockServer::start().await;

        let oauth = TwitchOauth::new(config::client_id(), config::client_secret())
            .with_client(config::client())
            .with_token_url(url::token(&server.uri()))
            .with_auth_url(url::auth(&server.uri()))
            .with_validate_url(url::validate(&server.uri()))
            .with_revoke_url(url::revoke(&server.uri()))
            .with_redirect_uri(url::redirect());

        (server, oauth)
    }
}

pub mod config {
    use asknothingx2_util::api::preset;
    use reqwest::Client;

    pub fn client_id() -> String {
        "hof5gwx0su6owfnys0yan9c87zr6t".to_string()
    }

    pub fn client_secret() -> String {
        "41vpdji4e9gif29md0ouet6fktd2".to_string()
    }

    pub fn client() -> Client {
        preset::testing("test/1.0").build().unwrap()
    }
}

pub mod validate {

    use super::config;

    pub fn form_client_id() -> String {
        format!("client_id={}", config::client_id())
    }

    pub fn form_client_secret() -> String {
        format!("client_secret={}", config::client_secret())
    }

    pub fn form_redirect() -> String {
        "redirect_uri=http%3A%2F%2Flocalhost%3A3000".to_string()
    }

    pub fn form_grant_type(kind: &str) -> String {
        format!("grant_type={kind}")
    }

    pub fn form_code(kind: &str) -> String {
        format!("code={kind}")
    }

    pub fn form_refresh_token() -> String {
        "refresh_token=eyJfaWQmNzMtNGCJ9%256VFV5LNrZFUj8oU231%2F3Aj".to_string()
    }
    pub fn form_token(token: &str) -> String {
        format!("token={token}")
    }
}

pub mod server {
    use serde_json::json;
    use twitch_oauth_token::{TwitchOauth, UserAuth};
    use wiremock::{
        Mock, MockServer, ResponseTemplate,
        matchers::{body_string_contains, header, method, path, query_param},
    };

    use super::{config, token, url, validate};

    pub async fn client_credentials(server: &MockServer) {
        Mock::given(method("POST"))
            .and(path("/oauth2/token"))
            .and(header("accept", "application/json"))
            .and(header("content-type", "application/x-www-form-urlencoded"))
            .and(body_string_contains(validate::form_client_id()))
            .and(body_string_contains(validate::form_client_secret()))
            .and(body_string_contains(validate::form_grant_type(
                "client_credentials",
            )))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "access_token": token::access(),
                "expires_in": 5011271,
                "token_type": "bearer"
            })))
            .mount(server)
            .await;
    }

    pub async fn authorization_code_grant_flow(
        server: &MockServer,
        oauth: &TwitchOauth<UserAuth>,
        scopes: &str,
    ) -> String {
        let auth_url = oauth.authorization_url().url();
        let state = auth_url
            .query_pairs()
            .find(|(k, _)| k == "state")
            .map(|(_, v)| v.to_string())
            .unwrap();

        Mock::given(method("GET"))
            .and(path("/oauth2/authorize"))
            .and(query_param("client_id", config::client_id()))
            .and(query_param("redirect_uri", url::redirect().to_string()))
            .and(query_param("response_type", "code"))
            .and(query_param("state", state.clone()))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "code": token::code(),
                "scope": scopes,
                "state": state
            })))
            .mount(server)
            .await;

        Mock::given(method("POST"))
            .and(path("/oauth2/token"))
            .and(header("accept", "application/json"))
            .and(header("content-type", "application/x-www-form-urlencoded"))
            .and(body_string_contains(validate::form_client_id()))
            .and(body_string_contains(validate::form_client_secret()))
            .and(body_string_contains(validate::form_code(&token::code())))
            .and(body_string_contains(validate::form_grant_type(
                "authorization_code",
            )))
            .and(body_string_contains(validate::form_redirect()))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "access_token": token::access(),
                "expires_in": 14124,
                "refresh_token": token::refresh(),
                "scope": [
                    "channel:moderate",
                    "chat:edit",
                    "chat:read"
                ],
                "token_type": "bearer"
            })))
            .mount(server)
            .await;

        state
    }

    pub async fn refresh_access_token(server: &MockServer) {
        Mock::given(method("POST"))
            .and(path("/oauth2/token"))
            .and(header("accept", "application/json"))
            .and(header("content-type", "application/x-www-form-urlencoded"))
            .and(body_string_contains(validate::form_client_id()))
            .and(body_string_contains(validate::form_client_secret()))
            .and(body_string_contains(validate::form_grant_type(
                "refresh_token",
            )))
            .and(body_string_contains(validate::form_refresh_token()))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "access_token": token::access(),
                "expires_in": 14124,
                "refresh_token": token::refresh(),
                "scope": [
                    "channel:moderate",
                    "chat:edit",
                    "chat:read"
                ],
                "token_type": "bearer"
            })))
            .mount(server)
            .await;
    }

    pub async fn validate_access_token(server: &MockServer) {
        Mock::given(method("GET"))
            .and(path("oauth2/validate"))
            .and(header(
                "Authorization",
                format!("OAuth {}", token::access().secret()),
            ))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
              "client_id": config::client_id(),
              "login": "twitchdev",
              "scopes": [
                "channel:read:subscriptions"
              ],
              "user_id": "141981764",
              "expires_in": 5520838
            })))
            .mount(server)
            .await;
    }

    pub async fn revoke_access_token(server: &MockServer) {
        Mock::given(method("POST"))
            .and(path("/oauth2/revoke"))
            .and(header("accept", "application/json"))
            .and(header("content-type", "application/x-www-form-urlencoded"))
            .and(body_string_contains(validate::form_client_id()))
            .and(body_string_contains(validate::form_token(
                token::access().secret(),
            )))
            .respond_with(ResponseTemplate::new(204))
            .mount(server)
            .await;
    }
}