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::{SendHttp, SendHttpError, SendHttpResult};
9use io_stream::io::StreamIo;
10use secrecy::{ExposeSecret, SecretString};
11use thiserror::Error;
12use url::form_urlencoded::Serializer;
13
14use super::issue_access_token::{
15    AccessTokenResponse, IssueAccessTokenErrorParams, IssueAccessTokenSuccessParams,
16};
17
18/// Errors that can occur during the coroutine progression.
19#[derive(Debug, Error)]
20pub enum RefreshOauth2AccessTokenError {
21    #[error(transparent)]
22    SendHttpRefresh(#[from] SendHttpError),
23    #[error(transparent)]
24    ParseHttpResponse(#[from] serde_json::Error),
25}
26
27/// Send result returned by the coroutine's resume function.
28#[derive(Debug)]
29pub enum RefreshOauth2AccessTokenResult {
30    /// The coroutine has successfully terminated its execution.
31    Ok(AccessTokenResponse),
32    /// The coroutine wants stream I/O.
33    Io(StreamIo),
34    /// The coroutine encountered an error.
35    Err(RefreshOauth2AccessTokenError),
36}
37
38/// The I/O-free coroutine to refresh an access token.
39///
40/// This coroutine sends the refresh access token HTTP request to the
41/// token endpoint and receives either a successful or an error HTTP
42/// response.
43///
44/// Refs: [`AccessTokenResponse`]
45pub struct RefreshOauth2AccessToken(SendHttp);
46
47impl RefreshOauth2AccessToken {
48    /// Creates a new I/O-free coroutine to refresh an access token.
49    pub fn new(
50        request: request::Builder,
51        body: RefreshAccessTokenParams<'_>,
52    ) -> http::Result<Self> {
53        let request = request
54            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
55            .body(body.to_string().into_bytes())?;
56
57        let send = SendHttp::new(request);
58        Ok(Self(send))
59    }
60
61    /// Makes the coroutine progress.
62    pub fn resume(&mut self, arg: Option<StreamIo>) -> RefreshOauth2AccessTokenResult {
63        let response = match self.0.resume(arg) {
64            SendHttpResult::Ok(result) => result.response,
65            SendHttpResult::Io(io) => return RefreshOauth2AccessTokenResult::Io(io),
66            SendHttpResult::Err(err) => return RefreshOauth2AccessTokenResult::Err(err.into()),
67        };
68
69        let body = response.body().as_slice();
70
71        if !response.status().is_success() {
72            return match IssueAccessTokenErrorParams::try_from(body) {
73                Ok(res) => RefreshOauth2AccessTokenResult::Ok(Err(res)),
74                Err(err) => RefreshOauth2AccessTokenResult::Err(err.into()),
75            };
76        }
77
78        match IssueAccessTokenSuccessParams::try_from(body) {
79            Ok(res) => RefreshOauth2AccessTokenResult::Ok(Ok(res)),
80            Err(err) => RefreshOauth2AccessTokenResult::Err(err.into()),
81        }
82    }
83}
84
85/// The refresh access token request parameters.
86///
87/// If the authorization server issued a refresh token to the client,
88/// the client makes a refresh request to the token endpoint by adding
89/// the following parameters using the
90/// "application/x-www-form-urlencoded" format with a character
91/// encoding of UTF-8 in the HTTP request entity-body.
92///
93/// Refs: https://datatracker.ietf.org/doc/html/rfc6749#section-6
94#[derive(Debug)]
95pub struct RefreshAccessTokenParams<'a> {
96    pub client_id: String,
97    pub refresh_token: SecretString,
98    pub scopes: HashSet<Cow<'a, str>>,
99}
100
101impl<'a> RefreshAccessTokenParams<'a> {
102    pub fn new(client_id: impl ToString, refresh_token: impl Into<SecretString>) -> Self {
103        Self {
104            client_id: client_id.to_string(),
105            refresh_token: refresh_token.into(),
106            scopes: HashSet::new(),
107        }
108    }
109
110    pub fn to_serializer(&self) -> Serializer<'a, String> {
111        let mut serializer = Serializer::new(String::new());
112
113        serializer.append_pair("grant_type", "refresh_token");
114        serializer.append_pair("client_id", &self.client_id);
115        serializer.append_pair("refresh_token", &self.refresh_token.expose_secret());
116
117        if !self.scopes.is_empty() {
118            let mut scope = String::new();
119            let mut glue = "";
120
121            for token in &self.scopes {
122                scope.push_str(glue);
123                scope.push_str(token);
124                glue = " ";
125            }
126
127            serializer.append_pair("scope", &scope);
128        }
129
130        serializer
131    }
132}
133
134impl ToString for RefreshAccessTokenParams<'_> {
135    fn to_string(&self) -> String {
136        self.to_serializer().finish()
137    }
138}