1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
use crate::AuthError;
use chrono::{DateTime, Duration, Utc};
use futures::Stream;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio_stream::wrappers::ReceiverStream;
const EXPIRY_BUFFER: i64 = 5 * 60;
/// Authentication method.
/// The Glimesh API requires an authentication method to be used.
/// The most basic is the ClientId method, which gives you read only access to the api.
#[derive(Debug, Clone)]
pub enum Auth {
/// Use Client-ID authentication.
/// When using this method, you can only 'read' from the API.
ClientId(String),
/// Use Bearer authentication.
/// The supplied access token is assumed to be valid.
/// If you would like the client to handle token refreshing, use [`Auth::RefreshableAccessToken`] instead.
AccessToken(String),
/// Use Bearer authentication.
/// This will use the provided refresh token to refresh the access token when/if it expires.
RefreshableAccessToken(RefreshableAccessToken),
/// Use Bearer authentication via the client_credentials flow.
///
/// This allows you to log in to the api as the account that created the developer application.
ClientCredentials(ClientCredentials),
}
impl Auth {
/// Use Client-ID authentication.
/// When using this method, you can only 'read' from the API.
pub fn client_id(client_id: impl Into<String>) -> Self {
Self::ClientId(client_id.into())
}
/// Use Bearer authentication.
/// The supplied access token is assumed to be valid.
/// If you would like the client to handle token refreshing, use [`Auth::RefreshableAccessToken`] instead.
pub fn access_token(access_token: impl Into<String>) -> Self {
Self::AccessToken(access_token.into())
}
/// Use Bearer authentication. This will use the provided refresh token to refresh the access
/// token when/if it expires.
///
/// You can listen for updates to the token using the Stream returned as the second part of the
/// tuple.
///
/// ## Example
///
/// ```rust
/// let (auth, token_receiver) = Auth::refreshable_access_token(
/// "<client_id>",
/// "<client_secret>",
/// "<redirect_uri>",
/// AccessToken { .. },
/// );
///
/// tokio::spawn(async move {
/// while let Some(token) = token_receiver.next().await {
/// println!("new token = {:#?}", token);
/// }
/// });
/// ```
///
/// # Panics
/// This function panics if the TLS backend cannot be initialized, or the resolver cannot load
/// the system configuration.
pub fn refreshable_access_token(
client_id: impl Into<String>,
client_secret: impl Into<String>,
redirect_uri: impl Into<String>,
access_token: AccessToken,
) -> (Self, impl Stream<Item = AccessToken>) {
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to create http client");
let (token_sender, token_receiver) = mpsc::channel(1);
(
Self::RefreshableAccessToken(RefreshableAccessToken {
client: ClientConfig {
client_id: client_id.into(),
client_secret: client_secret.into(),
redirect_uri: Some(redirect_uri.into()),
},
access_token: Arc::new(RwLock::new(access_token)),
http,
token_sender,
}),
ReceiverStream::new(token_receiver),
)
}
/// Use Bearer authentication via the client_credentials flow.
///
/// This allows you to log in to the api as the account that created the developer application.
///
/// # Panics
/// This function panics if the TLS backend cannot be initialized, or the resolver cannot load
/// the system configuration.
pub fn client_credentials(
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> Self {
Self::client_credentials_with_scopes(client_id, client_secret, vec![])
}
/// Use Bearer authentication via the client_credentials flow, specifying the scopes for the
/// token.
///
/// This allows you to log in to the api as the account that created the developer application.
///
/// # Panics
/// This function panics if the TLS backend cannot be initialized, or the resolver cannot load
/// the system configuration.
pub fn client_credentials_with_scopes(
client_id: impl Into<String>,
client_secret: impl Into<String>,
scopes: Vec<String>,
) -> Self {
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to create http client");
Self::ClientCredentials(ClientCredentials {
client: ClientConfig {
client_id: client_id.into(),
client_secret: client_secret.into(),
redirect_uri: None,
},
scopes,
access_token: Default::default(),
http,
})
}
}
/// Stored information about the access token
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessToken {
/// Glimesh access token
pub access_token: String,
/// Glimesh refresh token
pub refresh_token: Option<String>,
/// Time the token was created
#[serde(with = "glimesh_protocol::date")]
pub created_at: DateTime<Utc>,
/// Seconds after the created_at time when the token expires.
pub expires_in: i64,
/// Space separated list of scopes the token has permission for.
pub scope: Option<String>,
/// Type of the token, usually `bearer`
pub token_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// Glimesh client id, used to obtain an access token
pub client_id: String,
/// Glimesh client secret, used to obtain an access token
pub client_secret: String,
/// The same redirect uri that the token was obtained with. Can be None for client_credentials.
pub redirect_uri: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RefreshableAccessToken {
client: ClientConfig,
access_token: Arc<RwLock<AccessToken>>,
http: reqwest::Client,
token_sender: mpsc::Sender<AccessToken>,
}
impl RefreshableAccessToken {
/// Refresh the access token using the refresh token
pub async fn refresh(&self) -> Result<AccessToken, AuthError> {
let refresh_token = self
.access_token
.read()
.await
.refresh_token
.clone()
.ok_or(AuthError::MissingRefreshToken)?;
let res = self
.http
.post("https://glimesh.tv/api/oauth/token")
.form(&[
("grant_type", "refresh_token"),
("client_id", &self.client.client_id),
("client_secret", &self.client.client_secret),
(
"redirect_uri",
self.client
.redirect_uri
.as_ref()
.ok_or(AuthError::MissingRedirect)?,
),
("refresh_token", &refresh_token),
])
.send()
.await
.map_err(anyhow::Error::from)?;
if !res.status().is_success() {
return Err(AuthError::BadStatus(res.status().as_u16()));
}
let new_token: AccessToken = res.json().await.map_err(anyhow::Error::from)?;
{
let mut access_token = self.access_token.write().await;
*access_token = new_token.clone();
}
if let Err(err) = self.token_sender.send(new_token.clone()).await {
tracing::warn!(?err, "failed to send refresh access token to receiver");
}
Ok(new_token)
}
/// Obtain an access token, will refresh the token if near expiry.
pub async fn access_token(&self) -> Result<AccessToken, AuthError> {
let token = self.access_token.read().await.clone();
let expires_at = token.created_at + Duration::seconds(token.expires_in);
if (expires_at - Duration::seconds(EXPIRY_BUFFER)) > Utc::now() {
Ok(token)
} else {
self.refresh().await
}
}
}
#[derive(Debug, Clone)]
pub struct ClientCredentials {
client: ClientConfig,
access_token: Arc<RwLock<Option<AccessToken>>>,
scopes: Vec<String>,
http: reqwest::Client,
}
impl ClientCredentials {
/// Refresh or obtain a new access token using the client credentials
pub async fn refresh(&self) -> Result<AccessToken, AuthError> {
let res = self
.http
.post("https://glimesh.tv/api/oauth/token")
.form(&[
("grant_type", "client_credentials"),
("client_id", &self.client.client_id),
("client_secret", &self.client.client_secret),
("scope", &self.scopes.join(" ")),
])
.send()
.await
.map_err(anyhow::Error::from)?;
if !res.status().is_success() {
return Err(AuthError::BadStatus(res.status().as_u16()));
}
let new_token: AccessToken = res.json().await.map_err(anyhow::Error::from)?;
{
let mut access_token = self.access_token.write().await;
access_token.replace(new_token.clone());
}
Ok(new_token)
}
/// Obtain an access token, will refresh the token if near expiry.
pub async fn access_token(&self) -> Result<AccessToken, AuthError> {
let access_token = self.access_token.read().await.clone();
if let Some(token) = access_token {
let expires_at = token.created_at + Duration::seconds(token.expires_in);
if (expires_at - Duration::seconds(EXPIRY_BUFFER)) > Utc::now() {
return Ok(token);
}
}
self.refresh().await
}
}