Skip to content

Commit f83de41

Browse files
authored
Merge pull request #160 from twitch-rs/easy-refresh
easy refresh
2 parents e8c8c07 + cc05aa8 commit f83de41

File tree

7 files changed

+91
-9
lines changed

7 files changed

+91
-9
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
## [Unreleased] - ReleaseDate
66

7-
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.15.1...Unreleased)
7+
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.15.2...Unreleased)
8+
9+
## [v0.15.2] - 2025-03-05
10+
11+
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.15.1...v0.15.2)
812

913
### Added
1014

1115
- Added a way to see what scopes are missing in a `validator!()` with `Validator::missing`.
1216
- Added pretty printing for `validator!()`.
17+
- Added `UserToken::from_refresh_token` and `UserToken::from_existing_or_refresh_token`
1318

1419
## [v0.15.1] - 2025-01-12
1520

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[package]
22
name = "twitch_oauth2"
3-
version = "0.15.1"
3+
version = "0.15.2"
44
edition = "2021"
55
repository = "https://github.com/twitch-rs/twitch_oauth2"
66
license = "MIT OR Apache-2.0"
77
description = "Oauth2 for Twitch endpoints"
88
keywords = ["oauth", "twitch", "async", "asynchronous"]
9-
documentation = "https://docs.rs/twitch_oauth2/0.15.1"
9+
documentation = "https://docs.rs/twitch_oauth2/0.15.2"
1010
readme = "README.md"
1111
include = [
1212
"src/*",

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Twitch OAuth2 | OAuth2 for Twitch endpoints
22

3-
[![github]](https://github.com/twitch-rs/twitch_oauth2) [![crates-io]](https://crates.io/crates/twitch_oauth2) [![docs-rs]](https://docs.rs/twitch_oauth2/0.15.1/twitch_oauth2)
3+
[![github]](https://github.com/twitch-rs/twitch_oauth2) [![crates-io]](https://crates.io/crates/twitch_oauth2) [![docs-rs]](https://docs.rs/twitch_oauth2/0.15.2/twitch_oauth2)
44

55
[github]: https://img.shields.io/badge/github-twitch--rs/twitch__oauth2-8da0cb?style=for-the-badge&labelColor=555555&logo=github"
66
[crates-io]: https://img.shields.io/crates/v/twitch_oauth2.svg?style=for-the-badge&color=fc8d62&logo=rust"

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
//!
4949
//! ## HTTP Requests
5050
//!
51-
//! To enable client features with a supported http library, enable the http library feature in `twitch_oauth2`, like `twitch_oauth2 = { features = ["reqwest"], version = "0.15.1" }`.
51+
//! To enable client features with a supported http library, enable the http library feature in `twitch_oauth2`, like `twitch_oauth2 = { features = ["reqwest"], version = "0.15.2" }`.
5252
//! If you're using [twitch_api](https://crates.io/crates/twitch_api), you can use its [`HelixClient`](https://docs.rs/twitch_api/latest/twitch_api/struct.HelixClient.html) instead of the underlying http client.
5353
//!
5454
//!

src/tokens/errors.rs

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ impl ValidationError<std::convert::Infallible> {
3838
}
3939
}
4040

41+
/// Errors for [UserToken::from_refresh_token][crate::UserToken::from_refresh_token] and [UserToken::UserToken::from_existing_or_refresh_token][crate::UserToken::from_existing_or_refresh_token]
42+
#[derive(thiserror::Error, Debug, displaydoc::Display)]
43+
#[non_exhaustive]
44+
#[cfg(feature = "client")]
45+
pub enum RetrieveTokenError<RE: std::error::Error + Send + Sync + 'static> {
46+
/// could not validate token
47+
ValidationError(#[from] ValidationError<RE>),
48+
/// could not refresh token
49+
RefreshTokenError(#[from] RefreshTokenError<RE>),
50+
}
51+
4152
/// Errors for [AccessToken::revoke_token][crate::AccessTokenRef::revoke_token]
4253
#[allow(missing_docs)]
4354
#[derive(thiserror::Error, Debug, displaydoc::Display)]

src/tokens/user_token.rs

+69-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::errors::ValidationError;
44
#[cfg(feature = "client")]
55
use super::errors::{
66
DeviceUserTokenExchangeError, ImplicitUserTokenExchangeError, RefreshTokenError,
7-
UserTokenExchangeError,
7+
RetrieveTokenError, UserTokenExchangeError,
88
};
99
#[cfg(feature = "client")]
1010
use crate::client::Client;
@@ -112,9 +112,34 @@ impl UserToken {
112112
Self::from_existing(http_client, access_token, None, None).await
113113
}
114114

115-
/// Create a [UserToken] from an existing active user token. Retrieves [`login`](TwitchToken::login), [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes)
115+
/// Creates a [UserToken] using a refresh token. Retrieves the [`login`](TwitchToken::login) and [`scopes`](TwitchToken::scopes).
116116
///
117-
/// If the token is already expired, this function will fail to produce a [`UserToken`] and return [`ValidationError::NotAuthorized`]
117+
/// If an active user token is associated with the provided refresh token, this function will invalidate that existing user token.
118+
#[cfg(feature = "client")]
119+
pub async fn from_refresh_token<C>(
120+
http_client: &C,
121+
refresh_token: RefreshToken,
122+
client_id: ClientId,
123+
client_secret: impl Into<Option<ClientSecret>>,
124+
) -> Result<UserToken, RetrieveTokenError<<C as Client>::Error>>
125+
where
126+
C: Client,
127+
{
128+
let client_secret: Option<ClientSecret> = client_secret.into();
129+
let (access_token, _, refresh_token) = refresh_token
130+
.refresh_token(http_client, &client_id, client_secret.as_ref())
131+
.await?;
132+
Self::from_existing(http_client, access_token, refresh_token, client_secret)
133+
.await
134+
.map_err(Into::into)
135+
}
136+
137+
/// Create a [UserToken] from an existing active user token. Retrieves [`login`](TwitchToken::login) and [`scopes`](TwitchToken::scopes)
138+
///
139+
/// If the token is already expired, this function will fail to produce a [`UserToken`] and return [`ValidationError::NotAuthorized`].
140+
/// If you have a refresh token, you can use [`UserToken::from_refresh_token`] to refresh the token if was expired.
141+
///
142+
/// Consider using [`UserToken::from_existing_or_refresh_token`] to automatically refresh the token if it is expired.
118143
///
119144
/// # Examples
120145
///
@@ -150,6 +175,47 @@ impl UserToken {
150175
.map_err(|e| e.into_other())
151176
}
152177

178+
/// Create a [UserToken] from an existing active user token or refresh token if the access token is expired. Retrieves [`login`](TwitchToken::login), [`client_id`](TwitchToken::client_id) and [`scopes`](TwitchToken::scopes).
179+
///
180+
/// # Examples
181+
///
182+
/// ```rust,no_run
183+
/// use twitch_oauth2::{AccessToken, ClientId, ClientSecret, RefreshToken, UserToken};
184+
/// // Make sure you enable the feature "reqwest" for twitch_oauth2 if you want to use reqwest
185+
/// # async {let client = twitch_oauth2::client::DummyClient; stringify!(
186+
/// let client = reqwest::Client::builder()
187+
/// .redirect(reqwest::redirect::Policy::none())
188+
/// .build()?;
189+
/// # );
190+
/// let token = UserToken::from_existing_or_refresh_token(
191+
/// &client,
192+
/// AccessToken::from("my_access_token"),
193+
/// RefreshToken::from("my_refresh_token"),
194+
/// ClientId::from("my_client_id"),
195+
/// ClientSecret::from("my_optional_client_secret"),
196+
/// ).await?;
197+
/// # Ok::<(), Box<dyn std::error::Error>>(())};
198+
#[cfg(feature = "client")]
199+
pub async fn from_existing_or_refresh_token<C>(
200+
http_client: &C,
201+
access_token: AccessToken,
202+
refresh_token: RefreshToken,
203+
client_id: ClientId,
204+
client_secret: impl Into<Option<ClientSecret>>,
205+
) -> Result<UserToken, RetrieveTokenError<<C as Client>::Error>>
206+
where
207+
C: Client,
208+
{
209+
match access_token.validate_token(http_client).await {
210+
Ok(v) => Self::new(access_token, Some(refresh_token), v, client_secret)
211+
.map_err(|e| e.into_other().into()),
212+
Err(ValidationError::NotAuthorized) => {
213+
Self::from_refresh_token(http_client, refresh_token, client_id, client_secret).await
214+
}
215+
Err(e) => return Err(e.into()),
216+
}
217+
}
218+
153219
/// Assemble token without checks.
154220
///
155221
/// # Notes

0 commit comments

Comments
 (0)