io_oauth/2.0/authorization-code-grant/
access-token-request.rs

1use std::borrow::Cow;
2
3use http::{header::CONTENT_TYPE, request};
4use io_http::v1_1::coroutines::send::{SendHttp, SendHttpError, SendHttpResult};
5use io_stream::io::StreamIo;
6use thiserror::Error;
7use url::form_urlencoded::Serializer;
8
9use crate::v2_0::issue_access_token::{
10    AccessTokenResponse, IssueAccessTokenErrorParams, IssueAccessTokenSuccessParams,
11};
12
13#[cfg(feature = "pkce")]
14use super::pkce::PkceCodeVerifier;
15
16pub struct AccessTokenRequestParams<'a> {
17    pub code: Cow<'a, str>,
18    pub redirect_uri: Option<Cow<'a, str>>,
19    pub client_id: Cow<'a, str>,
20    #[cfg(feature = "pkce")]
21    pub pkce_code_verifier: Option<Cow<'a, PkceCodeVerifier>>,
22}
23
24impl<'a> AccessTokenRequestParams<'a> {
25    // SAFETY: this function exposes the code and the PKCE code
26    // verifier
27    pub fn to_form_url_encoded_serializer(&self) -> Serializer<'a, String> {
28        let mut serializer = Serializer::new(String::new());
29
30        serializer.append_pair("grant_type", "authorization_code");
31        serializer.append_pair("code", &self.code);
32
33        if let Some(uri) = &self.redirect_uri {
34            serializer.append_pair("redirect_uri", uri);
35        }
36
37        serializer.append_pair("client_id", &self.client_id);
38
39        #[cfg(feature = "pkce")]
40        if let Some(verifier) = &self.pkce_code_verifier {
41            let verifier = String::from_utf8_lossy(verifier.expose());
42            serializer.append_pair("code_verifier", &verifier);
43        }
44
45        serializer
46    }
47}
48
49impl ToString for AccessTokenRequestParams<'_> {
50    fn to_string(&self) -> String {
51        self.to_form_url_encoded_serializer().finish()
52    }
53}
54
55/// Errors that can occur during the coroutine progression.
56#[derive(Debug, Error)]
57pub enum RequestOauth2AccessTokenError {
58    #[error(transparent)]
59    SendHttpRequest(#[from] SendHttpError),
60    #[error(transparent)]
61    ParseHttpResponse(#[from] serde_json::Error),
62}
63
64/// Send result returned by the coroutine's resume function.
65#[derive(Debug)]
66pub enum RequestOauth2AccessTokenResult {
67    /// The coroutine has successfully terminated its execution.
68    Ok(AccessTokenResponse),
69    /// The coroutine wants stream I/O.
70    Io(StreamIo),
71    /// The coroutine encountered an error.
72    Err(RequestOauth2AccessTokenError),
73}
74
75/// The authorization code grant type is used to obtain both access
76/// tokens and refresh tokens and is optimized for confidential
77/// clients. Since this is a redirection-based flow, the client must
78/// be capable of interacting with the resource owner's user-agent
79/// (typically a web browser) and capable of receiving incoming
80/// requests (via redirection) from the authorization server.
81///
82/// Refs: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
83#[derive(Debug)]
84pub struct RequestOauth2AccessToken(SendHttp);
85
86impl RequestOauth2AccessToken {
87    pub fn new(
88        request: request::Builder,
89        body: AccessTokenRequestParams<'_>,
90    ) -> http::Result<Self> {
91        let request = request
92            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
93            .body(body.to_string().into_bytes())?;
94
95        Ok(Self(SendHttp::new(request)))
96    }
97
98    pub fn resume(&mut self, input: Option<StreamIo>) -> RequestOauth2AccessTokenResult {
99        let response = match self.0.resume(input) {
100            SendHttpResult::Ok(result) => result.response,
101            SendHttpResult::Io(io) => return RequestOauth2AccessTokenResult::Io(io),
102            SendHttpResult::Err(err) => return RequestOauth2AccessTokenResult::Err(err.into()),
103        };
104
105        let body = response.body().as_slice();
106
107        if !response.status().is_success() {
108            return match IssueAccessTokenErrorParams::try_from(body) {
109                Ok(res) => RequestOauth2AccessTokenResult::Ok(Err(res)),
110                Err(err) => RequestOauth2AccessTokenResult::Err(err.into()),
111            };
112        }
113
114        match IssueAccessTokenSuccessParams::try_from(body) {
115            Ok(res) => RequestOauth2AccessTokenResult::Ok(Ok(res)),
116            Err(err) => RequestOauth2AccessTokenResult::Err(err.into()),
117        }
118    }
119}