From 11ca64d32356595d5148f7a502bd4f2e92356ba4 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Fri, 11 Dec 2020 19:00:27 +0100 Subject: [PATCH 1/2] Add refresh ability to user tokens, and expose tokens for storage --- examples/user_token.rs | 4 +++ src/lib.rs | 2 +- src/tokens/user_token.rs | 54 +++++++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/examples/user_token.rs b/examples/user_token.rs index 1fa1b822..2f1b669f 100644 --- a/examples/user_token.rs +++ b/examples/user_token.rs @@ -13,6 +13,10 @@ async fn main() { .ok() .or_else(|| args.next()) .map(twitch_oauth2::RefreshToken::new), + std::env::var("TWITCH_CLIENT_SECRET") + .ok() + .or_else(|| args.next()) + .map(twitch_oauth2::ClientSecret::new), ) .await .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 03f19483..c4c4c8ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ //! # let reqwest_http_client = twitch_oauth2::dummy_http_client; // This is only here to fool doc tests //! let token = AccessToken::new("sometokenherewhichisvalidornot".to_string()); //! -//! match UserToken::from_existing(reqwest_http_client, token, None).await { +//! match UserToken::from_existing(reqwest_http_client, token, None, None).await { //! Ok(t) => println!("user_token: {}", t.token().secret()), //! Err(e) => panic!("got error: {}", e), //! } diff --git a/src/tokens/user_token.rs b/src/tokens/user_token.rs index e28b4825..8ada2006 100644 --- a/src/tokens/user_token.rs +++ b/src/tokens/user_token.rs @@ -2,6 +2,7 @@ use crate::tokens::{ errors::{RefreshTokenError, ValidationError}, Scope, TwitchToken, }; +use crate::ClientSecret; use oauth2::{AccessToken, ClientId, RefreshToken}; use oauth2::{HttpRequest, HttpResponse}; use std::future::Future; @@ -9,10 +10,13 @@ use std::future::Future; /// An User Token from the [OAuth implicit code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-implicit-code-flow) or [OAuth authorization code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-authorization-code-flow) #[derive(Debug, Clone)] pub struct UserToken { - access_token: AccessToken, + /// The access token used to authenticate requests with + pub access_token: AccessToken, client_id: ClientId, + client_secret: Option, login: Option, - refresh_token: Option, + /// The refresh token used to extend the life of this user token + pub refresh_token: Option, expires: Option, scopes: Vec, } @@ -23,13 +27,14 @@ impl UserToken { access_token: impl Into, refresh_token: impl Into>, client_id: impl Into, + client_secret: impl Into>, login: Option, scopes: Option>, - ) -> UserToken - { + ) -> 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, @@ -42,6 +47,7 @@ impl UserToken { http_client: C, access_token: AccessToken, refresh_token: impl Into>, + client_secret: impl Into>, ) -> Result> where RE: std::error::Error + Send + Sync + 'static, @@ -53,6 +59,7 @@ impl UserToken { access_token, refresh_token.into(), validated.client_id, + client_secret, validated.login, validated.scopes, )) @@ -61,21 +68,44 @@ impl UserToken { #[async_trait::async_trait(?Send)] impl TwitchToken for UserToken { - fn client_id(&self) -> &ClientId { &self.client_id } + fn client_id(&self) -> &ClientId { + &self.client_id + } - fn token(&self) -> &AccessToken { &self.access_token } + fn token(&self) -> &AccessToken { + &self.access_token + } - fn login(&self) -> Option<&str> { self.login.as_deref() } + fn login(&self) -> Option<&str> { + self.login.as_deref() + } - async fn refresh_token(&mut self, _: C) -> Result<(), RefreshTokenError> + async fn refresh_token(&mut self, http_client: C) -> Result<(), RefreshTokenError> where RE: std::error::Error + Send + Sync + 'static, C: FnOnce(HttpRequest) -> F, - F: Future>, { - Err(RefreshTokenError::NoRefreshToken) + F: Future>, + { + 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(()) } - fn expires(&self) -> Option { None } + fn expires(&self) -> Option { + None + } - fn scopes(&self) -> Option<&[Scope]> { Some(self.scopes.as_slice()) } + fn scopes(&self) -> Option<&[Scope]> { + Some(self.scopes.as_slice()) + } } From 4ab4571267c728bdc28f45a99b449fa27a149f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Gardstr=C3=B6m?= Date: Sat, 12 Dec 2020 20:59:40 +0100 Subject: [PATCH 2/2] add `RefreshTokenError::NoClientSecretFound` Also run cargo fmt with fix for newlines on multilined functions --- src/tokens/app_access_token.rs | 293 ++++++++++++++++----------------- src/tokens/errors.rs | 110 +++++++------ src/tokens/user_token.rs | 29 ++-- 3 files changed, 215 insertions(+), 217 deletions(-) diff --git a/src/tokens/app_access_token.rs b/src/tokens/app_access_token.rs index 5769bece..f5076a9c 100644 --- a/src/tokens/app_access_token.rs +++ b/src/tokens/app_access_token.rs @@ -1,147 +1,146 @@ -use super::errors::{TokenError, ValidationError}; -use crate::{ - id::TwitchClient, - tokens::{errors::RefreshTokenError, Scope, TwitchToken}, -}; -use oauth2::{AccessToken, AuthUrl, ClientId, ClientSecret, RefreshToken, TokenResponse}; -use oauth2::{HttpRequest, HttpResponse}; -use std::future::Future; - -/// An App Access Token from the [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow) -#[derive(Debug, Clone)] -pub struct AppAccessToken { - access_token: AccessToken, - refresh_token: Option, - expires: Option, - client_id: ClientId, - client_secret: ClientSecret, - login: Option, - scopes: Option>, -} - -#[async_trait::async_trait(?Send)] -impl TwitchToken for AppAccessToken { - 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( - &mut self, - http_client: C, - ) -> Result<(), RefreshTokenError> - where - RE: std::error::Error + Send + Sync + 'static, - C: FnOnce(HttpRequest) -> F, - F: Future>, - { - let (access_token, expires, refresh_token) = if let Some(token) = self.refresh_token.take() - { - crate::refresh_token(http_client, token, &self.client_id, &self.client_secret).await? - } else { - return Err(RefreshTokenError::NoRefreshToken); - }; - self.access_token = access_token; - self.expires = expires; - self.refresh_token = refresh_token; - Ok(()) - } - - fn expires(&self) -> Option { self.expires } - - fn scopes(&self) -> Option<&[Scope]> { self.scopes.as_deref() } -} - -impl AppAccessToken { - /// Assemble token without checks. - pub fn from_existing_unchecked( - access_token: AccessToken, - client_id: impl Into, - client_secret: impl Into, - login: Option, - scopes: Option>, - ) -> AppAccessToken - { - AppAccessToken { - access_token, - refresh_token: None, - client_id: client_id.into(), - client_secret: client_secret.into(), - login, - expires: None, - scopes, - } - } - - /// Assemble token and validate it. Retrieves [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes). - pub async fn from_existing( - http_client: C, - access_token: AccessToken, - client_secret: ClientSecret, - ) -> Result> - where - RE: std::error::Error + Send + Sync + 'static, - C: FnOnce(HttpRequest) -> F, - F: Future>, - { - let token = access_token; - let validated = crate::validate_token(http_client, &token).await?; - Ok(Self::from_existing_unchecked( - token, - validated.client_id, - client_secret, - None, - validated.scopes, - )) - } - - /// Generate app access token via [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow) - pub async fn get_app_access_token( - http_client: C, - client_id: ClientId, - client_secret: ClientSecret, - scopes: Vec, - ) -> Result> - where - RE: std::error::Error + Send + Sync + 'static, - C: Fn(HttpRequest) -> F, - F: Future>, - { - let now = std::time::Instant::now(); - let client = TwitchClient::new( - client_id.clone(), - Some(client_secret.clone()), - AuthUrl::new("https://id.twitch.tv/oauth2/authorize".to_owned()) - .expect("unexpected failure to parse auth url for app_access_token"), - Some(oauth2::TokenUrl::new( - "https://id.twitch.tv/oauth2/token".to_string(), - )?), - ); - let client = client.set_auth_type(oauth2::AuthType::RequestBody); - let mut client = client.exchange_client_credentials(); - for scope in scopes { - client = client.add_scope(scope.as_oauth_scope()); - } - let response = client - .request_async(&http_client) - .await - .map_err(TokenError::Request)?; - - let app_access = AppAccessToken { - access_token: response.access_token().clone(), - refresh_token: response.refresh_token().cloned(), - expires: response.expires_in().map(|dur| now + dur), - client_id, - client_secret, - login: None, - scopes: response - .scopes() - .cloned() - .map(|s| s.into_iter().map(|s| s.into()).collect()), - }; - - let _ = app_access.validate_token(http_client).await?; // Sanity check - Ok(app_access) - } -} +use super::errors::{TokenError, ValidationError}; +use crate::{ + id::TwitchClient, + tokens::{errors::RefreshTokenError, Scope, TwitchToken}, +}; +use oauth2::{AccessToken, AuthUrl, ClientId, ClientSecret, RefreshToken, TokenResponse}; +use oauth2::{HttpRequest, HttpResponse}; +use std::future::Future; + +/// An App Access Token from the [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow) +#[derive(Debug, Clone)] +pub struct AppAccessToken { + access_token: AccessToken, + refresh_token: Option, + expires: Option, + client_id: ClientId, + client_secret: ClientSecret, + login: Option, + scopes: Option>, +} + +#[async_trait::async_trait(?Send)] +impl TwitchToken for AppAccessToken { + 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( + &mut self, + http_client: C, + ) -> Result<(), RefreshTokenError> + where + RE: std::error::Error + Send + Sync + 'static, + C: FnOnce(HttpRequest) -> F, + F: Future>, + { + let (access_token, expires, refresh_token) = if let Some(token) = self.refresh_token.take() + { + crate::refresh_token(http_client, token, &self.client_id, &self.client_secret).await? + } else { + return Err(RefreshTokenError::NoRefreshToken); + }; + self.access_token = access_token; + self.expires = expires; + self.refresh_token = refresh_token; + Ok(()) + } + + fn expires(&self) -> Option { self.expires } + + fn scopes(&self) -> Option<&[Scope]> { self.scopes.as_deref() } +} + +impl AppAccessToken { + /// Assemble token without checks. + pub fn from_existing_unchecked( + access_token: AccessToken, + client_id: impl Into, + client_secret: impl Into, + login: Option, + scopes: Option>, + ) -> AppAccessToken { + AppAccessToken { + access_token, + refresh_token: None, + client_id: client_id.into(), + client_secret: client_secret.into(), + login, + expires: None, + scopes, + } + } + + /// Assemble token and validate it. Retrieves [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes). + pub async fn from_existing( + http_client: C, + access_token: AccessToken, + client_secret: ClientSecret, + ) -> Result> + where + RE: std::error::Error + Send + Sync + 'static, + C: FnOnce(HttpRequest) -> F, + F: Future>, + { + let token = access_token; + let validated = crate::validate_token(http_client, &token).await?; + Ok(Self::from_existing_unchecked( + token, + validated.client_id, + client_secret, + None, + validated.scopes, + )) + } + + /// Generate app access token via [OAuth client credentials flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow) + pub async fn get_app_access_token( + http_client: C, + client_id: ClientId, + client_secret: ClientSecret, + scopes: Vec, + ) -> Result> + where + RE: std::error::Error + Send + Sync + 'static, + C: Fn(HttpRequest) -> F, + F: Future>, + { + let now = std::time::Instant::now(); + let client = TwitchClient::new( + client_id.clone(), + Some(client_secret.clone()), + AuthUrl::new("https://id.twitch.tv/oauth2/authorize".to_owned()) + .expect("unexpected failure to parse auth url for app_access_token"), + Some(oauth2::TokenUrl::new( + "https://id.twitch.tv/oauth2/token".to_string(), + )?), + ); + let client = client.set_auth_type(oauth2::AuthType::RequestBody); + let mut client = client.exchange_client_credentials(); + for scope in scopes { + client = client.add_scope(scope.as_oauth_scope()); + } + let response = client + .request_async(&http_client) + .await + .map_err(TokenError::Request)?; + + let app_access = AppAccessToken { + access_token: response.access_token().clone(), + refresh_token: response.refresh_token().cloned(), + expires: response.expires_in().map(|dur| now + dur), + client_id, + client_secret, + login: None, + scopes: response + .scopes() + .cloned() + .map(|s| s.into_iter().map(|s| s.into()).collect()), + }; + + let _ = app_access.validate_token(http_client).await?; // Sanity check + Ok(app_access) + } +} diff --git a/src/tokens/errors.rs b/src/tokens/errors.rs index 5c22340c..0b6ae7f8 100644 --- a/src/tokens/errors.rs +++ b/src/tokens/errors.rs @@ -1,53 +1,57 @@ -//! Errors - -use crate::id::TwitchTokenErrorResponse; -use oauth2::HttpResponse as OAuth2HttpResponse; -use oauth2::RequestTokenError; -/// General errors for talking with twitch, currently only used in [AppAccessToken::get_app_access_token][crate::tokens::AppAccessToken::get_app_access_token] -#[allow(missing_docs)] -#[derive(thiserror::Error, Debug, displaydoc::Display)] -pub enum TokenError { - /// request for token failed. {0} - Request(RequestTokenError), - /// could not parse url - ParseError(#[from] oauth2::url::ParseError), - /// could not get validation for token - ValidationError(#[from] ValidationError), -} - -/// Errors for [validate_token][crate::validate_token] -#[derive(thiserror::Error, Debug, displaydoc::Display)] -pub enum ValidationError { - /// deserializations failed - DeserializeError(#[from] serde_json::Error), - /// token is not authorized for use - NotAuthorized, - /// twitch returned an unexpected status: {0} - TwitchError(TwitchTokenErrorResponse), - /// failed to request validation: {0} - Request(#[source] RE), -} - -/// Errors for [revoke_token][crate::revoke_token] -#[allow(missing_docs)] -#[derive(thiserror::Error, Debug, displaydoc::Display)] -pub enum RevokeTokenError { - /// 400 Bad Request: {0} - BadRequest(String), - /// failed to do revokation: {0} - RequestError(#[source] RE), - /// got unexpected return: {0:?} - Other(OAuth2HttpResponse), -} - -/// Errors for [TwitchToken::refresh_token][crate::TwitchToken::refresh_token] -#[allow(missing_docs)] -#[derive(thiserror::Error, Debug, displaydoc::Display)] -pub enum RefreshTokenError { - /// request for token failed. {0} - RequestError(#[source] RequestTokenError), - /// could not parse url - ParseError(#[from] oauth2::url::ParseError), - /// no refresh token found - NoRefreshToken, -} +//! Errors + +use crate::id::TwitchTokenErrorResponse; +use oauth2::HttpResponse as OAuth2HttpResponse; +use oauth2::RequestTokenError; +/// General errors for talking with twitch, currently only used in [AppAccessToken::get_app_access_token][crate::tokens::AppAccessToken::get_app_access_token] +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug, displaydoc::Display)] +pub enum TokenError { + /// request for token failed. {0} + Request(RequestTokenError), + /// could not parse url + ParseError(#[from] oauth2::url::ParseError), + /// could not get validation for token + ValidationError(#[from] ValidationError), +} + +/// Errors for [validate_token][crate::validate_token] +#[derive(thiserror::Error, Debug, displaydoc::Display)] +pub enum ValidationError { + /// deserializations failed + DeserializeError(#[from] serde_json::Error), + /// token is not authorized for use + NotAuthorized, + /// twitch returned an unexpected status: {0} + TwitchError(TwitchTokenErrorResponse), + /// failed to request validation: {0} + Request(#[source] RE), +} + +/// Errors for [revoke_token][crate::revoke_token] +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug, displaydoc::Display)] +pub enum RevokeTokenError { + /// 400 Bad Request: {0} + BadRequest(String), + /// failed to do revokation: {0} + RequestError(#[source] RE), + /// got unexpected return: {0:?} + Other(OAuth2HttpResponse), +} + +/// Errors for [TwitchToken::refresh_token][crate::TwitchToken::refresh_token] +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug, displaydoc::Display)] +pub enum RefreshTokenError { + /// request for token failed. {0} + RequestError(#[source] RequestTokenError), + /// could not parse url + ParseError(#[from] oauth2::url::ParseError), + /// no client secret found + /// + /// A client secret is needed to request a refreshed token. + NoClientSecretFound, + /// no refresh token found + NoRefreshToken, +} diff --git a/src/tokens/user_token.rs b/src/tokens/user_token.rs index 8ada2006..faac80f7 100644 --- a/src/tokens/user_token.rs +++ b/src/tokens/user_token.rs @@ -68,19 +68,16 @@ impl UserToken { #[async_trait::async_trait(?Send)] impl TwitchToken for UserToken { - fn client_id(&self) -> &ClientId { - &self.client_id - } + fn client_id(&self) -> &ClientId { &self.client_id } - fn token(&self) -> &AccessToken { - &self.access_token - } + fn token(&self) -> &AccessToken { &self.access_token } - fn login(&self) -> Option<&str> { - self.login.as_deref() - } + fn login(&self) -> Option<&str> { self.login.as_deref() } - async fn refresh_token(&mut self, http_client: C) -> Result<(), RefreshTokenError> + async fn refresh_token( + &mut self, + http_client: C, + ) -> Result<(), RefreshTokenError> where RE: std::error::Error + Send + Sync + 'static, C: FnOnce(HttpRequest) -> F, @@ -97,15 +94,13 @@ impl TwitchToken for UserToken { self.access_token = access_token; self.expires = expires; self.refresh_token = refresh_token; + Ok(()) + } else { + return Err(RefreshTokenError::NoClientSecretFound); } - Ok(()) } - fn expires(&self) -> Option { - None - } + fn expires(&self) -> Option { None } - fn scopes(&self) -> Option<&[Scope]> { - Some(self.scopes.as_slice()) - } + fn scopes(&self) -> Option<&[Scope]> { Some(self.scopes.as_slice()) } }