io_oauth/2.0/
refresh-access-token.rs

1//! Module dedicated to the section 6: Refreshing an Access Token.
2//!
3//! Refs: https://datatracker.ietf.org/doc/html/rfc6749#section-6
4
5use std::{borrow::Cow, collections::HashSet};
6
7use http::{header::CONTENT_TYPE, request};
8use io_http::v1_1::coroutines::Send;
9use io_stream::Io;
10use secrecy::{ExposeSecret, SecretString};
11use url::form_urlencoded::Serializer;
12
13use super::issue_access_token::{
14    AccessTokenResponse, IssueAccessTokenErrorParams, IssueAccessTokenSuccessParams,
15};
16
17/// The I/O-free coroutine to refresh an access token.
18///
19/// This coroutine sends the refresh access token HTTP request to the
20/// token endpoint and receives either a successful or an error HTTP
21/// response.
22///
23/// Refs: [`AccessTokenResponse`]
24pub struct RefreshAccessToken(Send);
25
26impl RefreshAccessToken {
27    /// Creates a new I/O-free coroutine to refresh an access token.
28    pub fn new(
29        request: request::Builder,
30        body: RefreshAccessTokenParams<'_>,
31    ) -> http::Result<Self> {
32        let request = request
33            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
34            .body(body.to_string().into_bytes())?;
35
36        let send = Send::new(request);
37        Ok(Self(send))
38    }
39
40    /// Makes the coroutine progress.
41    pub fn resume(
42        &mut self,
43        input: Option<Io>,
44    ) -> Result<serde_json::Result<AccessTokenResponse>, Io> {
45        let response = self.0.resume(input)?;
46        let body = response.body().as_slice();
47
48        if response.status().is_success() {
49            match IssueAccessTokenSuccessParams::try_from(body) {
50                Ok(res) => Ok(Ok(Ok(res))),
51                Err(err) => Ok(Err(err)),
52            }
53        } else {
54            match IssueAccessTokenErrorParams::try_from(body) {
55                Ok(res) => Ok(Ok(Err(res))),
56                Err(err) => Ok(Err(err)),
57            }
58        }
59    }
60}
61
62/// The refresh access token request parameters.
63///
64/// If the authorization server issued a refresh token to the client,
65/// the client makes a refresh request to the token endpoint by adding
66/// the following parameters using the
67/// "application/x-www-form-urlencoded" format with a character
68/// encoding of UTF-8 in the HTTP request entity-body.
69///
70/// Refs: https://datatracker.ietf.org/doc/html/rfc6749#section-6
71#[derive(Debug)]
72pub struct RefreshAccessTokenParams<'a> {
73    pub client_id: String,
74    pub refresh_token: SecretString,
75    pub scopes: HashSet<Cow<'a, str>>,
76}
77
78impl<'a> RefreshAccessTokenParams<'a> {
79    pub fn new(client_id: impl ToString, refresh_token: impl Into<SecretString>) -> Self {
80        Self {
81            client_id: client_id.to_string(),
82            refresh_token: refresh_token.into(),
83            scopes: HashSet::new(),
84        }
85    }
86
87    pub fn to_serializer(&self) -> Serializer<'a, String> {
88        let mut serializer = Serializer::new(String::new());
89
90        serializer.append_pair("grant_type", "refresh_token");
91        serializer.append_pair("client_id", &self.client_id);
92        serializer.append_pair("refresh_token", &self.refresh_token.expose_secret());
93
94        if !self.scopes.is_empty() {
95            let mut scope = String::new();
96            let mut glue = "";
97
98            for token in &self.scopes {
99                scope.push_str(glue);
100                scope.push_str(token);
101                glue = " ";
102            }
103
104            serializer.append_pair("scope", &scope);
105        }
106
107        serializer
108    }
109}
110
111impl ToString for RefreshAccessTokenParams<'_> {
112    fn to_string(&self) -> String {
113        self.to_serializer().finish()
114    }
115}