Skip to content

Commit f657e9b

Browse files
committed
better expiration handling
1 parent 34de0bf commit f657e9b

File tree

5 files changed

+143
-51
lines changed

5 files changed

+143
-51
lines changed

CHANGELOG.md

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,31 @@
88

99
### Breaking
1010

11-
* Added new feature flag `client` that enables client specific functions. Without this feature,
11+
- Added new feature flag `client` that enables client specific functions. Without this feature,
1212
`twitch_oauth2` will only provide non-async functions and
13-
provide library users `http::Request`s and consume `http::Response`s.
13+
provide library users functions that returns `http::Request`s and consume `http::Response`s.
14+
- `ValidatedToken::expires_in` is now an `Option`.
1415

1516
## [v0.8.0] - 2022-08-27
1617

1718
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.7.1...v0.8.0)
1819

1920
### Breaking
2021

21-
* Bumped `aliri_braid` to `0.2`, this change means that the `new` method on the types in `types` only take an owned string now
22-
* `AccessToken::new`, `ClientId::new`, `ClientSecret::new`, `CsrfToken::new` and `RefreshToken::new` now take a `String` instead of `impl Into<String>`
22+
- Bumped `aliri_braid` to `0.2`, this change means that the `new` method on the types in `types` only take an owned string now
23+
- `AccessToken::new`, `ClientId::new`, `ClientSecret::new`, `CsrfToken::new` and `RefreshToken::new` now take a `String` instead of `impl Into<String>`
2324

2425
## [v0.7.1] - 2022-08-27
2526

2627
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.7.0...v0.7.1)
2728

2829
### Changed
2930

30-
* Organization moved to `twitch-rs`
31+
- Organization moved to `twitch-rs`
3132

3233
### Added
3334

34-
* Added scopes `channel:manage:raids`, `channel:manage:moderators`, `channel:manage:vips`, `channel:read:charity`,
35+
- Added scopes `channel:manage:raids`, `channel:manage:moderators`, `channel:manage:vips`, `channel:read:charity`,
3536
`channel:read:vips`, `moderator:manage:announcements`, `moderator:manage:chat_messages`, `user:manage:chat_color` and
3637
`user:manage:whispers`
3738

@@ -41,16 +42,16 @@
4142

4243
### Breaking changes
4344

44-
* switch to [`twitch_types`](https://crates.io/crates/twitch_types) for `UserId` and `Nickname`/`UserName`
45-
* bump MSRV to 1.60, also changes the feature names for clients to their simpler variant `surf` and `client`
45+
- switch to [`twitch_types`](https://crates.io/crates/twitch_types) for `UserId` and `Nickname`/`UserName`
46+
- bump MSRV to 1.60, also changes the feature names for clients to their simpler variant `surf` and `client`
4647

4748
## [v0.6.1] - 2021-11-23
4849

4950
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.6.0...v0.6.1)
5051

5152
### Added
5253

53-
* Added new scopes `moderator:manage:automod_settings`, `moderator:manage:banned_users`,
54+
- Added new scopes `moderator:manage:automod_settings`, `moderator:manage:banned_users`,
5455
`moderator:manage:blocked_terms`, `moderator:manage:chat_settings`, `moderator:read:automod_settings`,
5556
`moderator:read:blocked_terms` and `moderator:read:chat_settings`
5657

@@ -60,68 +61,69 @@
6061

6162
### Breaking changes
6263

63-
* All types associated with tokens are now defined in this crate. This is a consequence of the `oauth2` dependency being removed from tree.
64+
- All types associated with tokens are now defined in this crate. This is a consequence of the `oauth2` dependency being removed from tree.
6465
Additionally, as another consequence, clients are now able to be specified as a `for<'a> &'a T where T: Client<'a>`, meaning `twitch_api` can use its clients as an interface to token requests,
6566
and clients can persist instead of being rebuilt every call. Care should be taken when making clients, as SSRF and similar attacks are possible with improper client configurations.
6667

6768
### Added
6869

69-
* Added types/braids `ClientId`, `ClientSecret`, `AccessToken`, `RefreshToken` and `CsrfToken`.
70-
* Added way to interact with the Twitch-CLI [mock API](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) using environment variables.
70+
- Added types/braids `ClientId`, `ClientSecret`, `AccessToken`, `RefreshToken` and `CsrfToken`.
71+
- Added way to interact with the Twitch-CLI [mock API](https://github.com/twitchdev/twitch-cli/blob/main/docs/mock-api.md) using environment variables.
7172
See static variables `AUTH_URL`, `TOKEN_URL`, `VALIDATE_URL` and `REVOKE_URL` for more information.
72-
* Added `impl Borrow<str> for Scope`, meaning it can be used in places it couldn't be used before. Primarily, it allows the following code to work:
73+
- Added `impl Borrow<str> for Scope`, meaning it can be used in places it couldn't be used before. Primarily, it allows the following code to work:
7374
```rust
7475
let scopes = vec![Scope::ChatEdit, Scope::ChatRead];
7576
let space_separated_scope: String = scopes.as_slice().join(" ");
7677
```
77-
* Added scope `channel:read:goals`
78+
- Added scope `channel:read:goals`
7879

7980
### Changed
8081

81-
* Requests to `id.twitch.tv` now follow the documentation, instead of following a subset of the RFC for oauth2.
82-
* URLs are now initialized lazily and specified as `url::Url`s.
82+
- Requests to `id.twitch.tv` now follow the documentation, instead of following a subset of the RFC for oauth2.
83+
- URLs are now initialized lazily and specified as `url::Url`s.
8384

8485
### Removed
8586

86-
* Removed `oauth2` dependency.
87+
- Removed `oauth2` dependency.
8788

8889
## [v0.5.2] - 2021-06-18
8990

9091
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.5.1...v0.5.2)
9192

9293
### Added
9394

94-
* Added new scope `channel:manage:schedule`
95+
- Added new scope `channel:manage:schedule`
9596

9697
## [v0.5.1] - 2021-05-16
9798

9899
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/v0.5.0...v0.5.1)
99100

100101
### Added
101102

102-
* Added new scopes `channel:manage:polls`, `channel:manage:predictions`, `channel:read:polls`, `channel:read:predictions`, and `moderator:manage:automod`,
103-
* Added function `Scope::description` to get the description of the scope
103+
- Added new scopes `channel:manage:polls`, `channel:manage:predictions`, `channel:read:polls`, `channel:read:predictions`, and `moderator:manage:automod`,
104+
- Added function `Scope::description` to get the description of the scope
104105

105106
## [v0.5.0] - 2021-05-08
106107

107108
[Commits](https://github.com/twitch-rs/twitch_oauth2/compare/49a083ceda6768cc52a1f8f1714bb7f942f24c01...v0.5.0)
108109

109110
### Added
110111

111-
* Made crate runtime agnostic with custom clients.
112-
* Updated deps.
113-
* Add an extra (optional) client secret field to `UserToken::from_existing` (thanks [Dinnerbone](https://github.com/Dinnerbone))
114-
* Added `channel:manage:redemptions`, `channel:read:editors`, `channel:manage:videos`, `user:read:blocked_users`, `user:manage:blocked_users`, `user:read:subscriptions` and `user:read:follows`
115-
* Implemented [OAuth Authorization Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-authorization-code-flow) with `UserTokenBuilder`
116-
* Added a way to suggest or infer that an user token is never expiring, making `is_elapsed` return false and `expires_in` a bogus (max) duration.
112+
- Made crate runtime agnostic with custom clients.
113+
- Updated deps.
114+
- Add an extra (optional) client secret field to `UserToken::from_existing` (thanks [Dinnerbone](https://github.com/Dinnerbone))
115+
- Added `channel:manage:redemptions`, `channel:read:editors`, `channel:manage:videos`, `user:read:blocked_users`, `user:manage:blocked_users`, `user:read:subscriptions` and `user:read:follows`
116+
- Implemented [OAuth Authorization Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#oauth-authorization-code-flow) with `UserTokenBuilder`
117+
- Added a way to suggest or infer that an user token is never expiring, making `is_elapsed` return false and `expires_in` a bogus (max) duration.
118+
117119
### Changed
118120

119-
* MSRV: 1.51
120-
* Made scope take `Cow<&'static str>`
121-
* Made fields `access_token`, `refresh_token`, `user_id` and `login` `pub` on `UserToken` and `AppAccessToken` (where applicable)
122-
* Fixed wrong scope `user:read:stream_key` -> `channel:read:stream_key`
123-
* BREAKING: changed `TwitchToken::expires` -> `TwitchToken::expires_in` to calculate current lifetime of token
121+
- MSRV: 1.51
122+
- Made scope take `Cow<&'static str>`
123+
- Made fields `access_token`, `refresh_token`, `user_id` and `login` `pub` on `UserToken` and `AppAccessToken` (where applicable)
124+
- Fixed wrong scope `user:read:stream_key` -> `channel:read:stream_key`
125+
- BREAKING: changed `TwitchToken::expires` -> `TwitchToken::expires_in` to calculate current lifetime of token
124126

125127
## End of Changelog
126128

127-
Changelog starts on v0.5.0
129+
Changelog starts on v0.5.0

src/id.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ pub struct TwitchTokenResponse {
2929
impl TwitchTokenResponse {
3030
/// Create a [TwitchTokenResponse] from a [http::Response]
3131
pub fn from_response<B: AsRef<[u8]>>(
32-
resp: &http::Response<B>,
32+
response: &http::Response<B>,
3333
) -> Result<TwitchTokenResponse, RequestParseError> {
34-
crate::parse_response(resp)
34+
crate::parse_response(response)
3535
}
3636
}
3737

src/tokens.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,29 @@ pub struct ValidatedToken {
163163
/// Scopes attached to the token.
164164
pub scopes: Option<Vec<Scope>>,
165165
/// Lifetime of the token
166-
#[serde(deserialize_with = "seconds_to_duration")]
167-
pub expires_in: std::time::Duration,
166+
#[serde(deserialize_with = "expires_in")]
167+
pub expires_in: Option<std::time::Duration>,
168168
}
169169

170-
fn seconds_to_duration<'a, D: serde::de::Deserializer<'a>>(
170+
fn expires_in<'a, D: serde::de::Deserializer<'a>>(
171171
d: D,
172-
) -> Result<std::time::Duration, D::Error> {
173-
Ok(std::time::Duration::from_secs(u64::deserialize(d)?))
172+
) -> Result<Option<std::time::Duration>, D::Error> {
173+
let num = u64::deserialize(d)?;
174+
if num == 0 {
175+
Ok(None)
176+
} else {
177+
Ok(Some(std::time::Duration::from_secs(num)))
178+
}
174179
}
180+
175181
impl ValidatedToken {
176182
/// Assemble a a validated token from a response.
177183
///
178184
/// Get the request that generates this response with [`AccessToken::validate_token_request`][crate::types::AccessTokenRef::validate_token_request]
179185
pub fn from_response<B: AsRef<[u8]>>(
180-
resp: &http::Response<B>,
186+
response: &http::Response<B>,
181187
) -> Result<ValidatedToken, ValidationError<std::convert::Infallible>> {
182-
match crate::parse_response(resp) {
188+
match crate::parse_response(response) {
183189
Ok(ok) => Ok(ok),
184190
Err(err) => match err {
185191
RequestParseError::TwitchError(TwitchTokenErrorResponse { status, .. })
@@ -192,3 +198,58 @@ impl ValidatedToken {
192198
}
193199
}
194200
}
201+
202+
#[cfg(test)]
203+
mod tests {
204+
use crate::ValidatedToken;
205+
206+
use super::errors::ValidationError;
207+
208+
#[test]
209+
fn validated_token() {
210+
let body = br#"
211+
{
212+
"client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
213+
"login": "twitchdev",
214+
"scopes": [
215+
"channel:read:subscriptions"
216+
],
217+
"user_id": "141981764",
218+
"expires_in": 5520838
219+
}
220+
"#;
221+
let response = http::Response::builder().status(200).body(body).unwrap();
222+
ValidatedToken::from_response(&response).unwrap();
223+
}
224+
225+
#[test]
226+
fn validated_non_expiring_token() {
227+
let body = br#"
228+
{
229+
"client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
230+
"login": "twitchdev",
231+
"scopes": [
232+
"channel:read:subscriptions"
233+
],
234+
"user_id": "141981764",
235+
"expires_in": 0
236+
}
237+
"#;
238+
let response = http::Response::builder().status(200).body(body).unwrap();
239+
let token = ValidatedToken::from_response(&response).unwrap();
240+
assert!(token.expires_in.is_none());
241+
}
242+
243+
#[test]
244+
fn validated_error_response() {
245+
let body = br#"
246+
{
247+
"status": 401,
248+
"message": "missing authorization token",
249+
}
250+
"#;
251+
let response = http::Response::builder().status(401).body(body).unwrap();
252+
let error = ValidatedToken::from_response(&response).unwrap_err();
253+
assert!(matches!(error, ValidationError::RequestParseError(_)))
254+
}
255+
}

src/tokens/app_access_token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ impl AppAccessToken {
131131
validated.client_id,
132132
client_secret,
133133
validated.scopes,
134-
Some(validated.expires_in),
134+
validated.expires_in,
135135
))
136136
}
137137

src/tokens/user_token.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,7 @@ impl UserToken {
7070
validated.login.ok_or(ValidationError::NoLogin)?,
7171
validated.user_id.ok_or(ValidationError::NoLogin)?,
7272
validated.scopes,
73-
Some(validated.expires_in).filter(|d| {
74-
// FIXME: https://github.com/rust-lang/rust/pull/84084
75-
// FIXME: nanos are not returned
76-
// if duration is zero, the token will never expire. if the token was expired, twitch would return NotAuthorized
77-
// TODO: There could be a situation where this fails, if the token is just about to expire, say 500ms, does twitch round up to 1 or down to 0?
78-
!(d.as_secs() == 0 && d.as_nanos() == 0)
79-
}),
73+
validated.expires_in,
8074
))
8175
}
8276

@@ -683,18 +677,53 @@ impl ImplicitUserTokenBuilder {
683677

684678
#[cfg(test)]
685679
mod tests {
680+
use crate::id::TwitchTokenResponse;
681+
686682
pub use super::*;
683+
684+
#[test]
685+
fn from_validated_and_token() {
686+
let body = br#"
687+
{
688+
"client_id": "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
689+
"login": "twitchdev",
690+
"scopes": [
691+
"channel:read:subscriptions"
692+
],
693+
"user_id": "141981764",
694+
"expires_in": 5520838
695+
}
696+
"#;
697+
let response = http::Response::builder().status(200).body(body).unwrap();
698+
let validated = ValidatedToken::from_response(&response).unwrap();
699+
let body = br#"
700+
{
701+
"access_token": "rfx2uswqe8l4g1mkagrvg5tv0ks3",
702+
"expires_in": 14124,
703+
"refresh_token": "5b93chm6hdve3mycz05zfzatkfdenfspp1h1ar2xxdalen01",
704+
"scope": [
705+
"channel:read:subscriptions"
706+
],
707+
"token_type": "bearer"
708+
}
709+
"#;
710+
let response = http::Response::builder().status(200).body(body).unwrap();
711+
let response = TwitchTokenResponse::from_response(&response).unwrap();
712+
713+
UserToken::from_response(response, validated, None).unwrap();
714+
}
715+
687716
#[test]
688717
fn generate_url() {
689-
dbg!(UserTokenBuilder::new(
718+
UserTokenBuilder::new(
690719
ClientId::from("random_client"),
691720
ClientSecret::from("random_secret"),
692721
url::Url::parse("https://localhost").unwrap(),
693722
)
694723
.force_verify(true)
695724
.generate_url()
696725
.0
697-
.to_string());
726+
.to_string();
698727
}
699728

700729
#[tokio::test]

0 commit comments

Comments
 (0)