oauth2_core/authorization_code_grant/
authorization_request.rs

1//! https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
2
3use http::Method;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use crate::types::{
8    ClientId, CodeChallenge, CodeChallengeMethod, Nonce, Scope, ScopeFromStrError, ScopeParameter,
9    State,
10};
11
12pub const METHOD: Method = Method::GET;
13pub const RESPONSE_TYPE: &str = "code";
14
15//
16//
17//
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct Query<SCOPE>
20where
21    SCOPE: Scope,
22{
23    pub response_type: String,
24    pub client_id: ClientId,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub redirect_uri: Option<String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub scope: Option<ScopeParameter<SCOPE>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub state: Option<State>,
31
32    // PKCE
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub code_challenge: Option<CodeChallenge>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub code_challenge_method: Option<CodeChallengeMethod>,
37
38    // OIDC
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub nonce: Option<Nonce>,
41
42    #[serde(flatten, skip_serializing_if = "Option::is_none")]
43    _extra: Option<Map<String, Value>>,
44}
45impl<SCOPE> Query<SCOPE>
46where
47    SCOPE: Scope,
48{
49    pub fn new(
50        client_id: ClientId,
51        redirect_uri: Option<String>,
52        scope: Option<ScopeParameter<SCOPE>>,
53        state: Option<State>,
54    ) -> Self {
55        Self::internal_new(client_id, redirect_uri, scope, state, None, None)
56    }
57
58    fn internal_new(
59        client_id: ClientId,
60        redirect_uri: Option<String>,
61        scope: Option<ScopeParameter<SCOPE>>,
62        state: Option<State>,
63        code_challenge: Option<(CodeChallenge, CodeChallengeMethod)>,
64        nonce: Option<Nonce>,
65    ) -> Self {
66        Self {
67            response_type: RESPONSE_TYPE.to_owned(),
68            client_id,
69            redirect_uri,
70            scope,
71            state,
72            code_challenge: code_challenge.to_owned().map(|x| x.0),
73            code_challenge_method: code_challenge.map(|x| x.1),
74            nonce,
75            _extra: None,
76        }
77    }
78
79    pub fn set_extra(&mut self, extra: Map<String, Value>) {
80        self._extra = Some(extra);
81    }
82    pub fn extra(&self) -> Option<&Map<String, Value>> {
83        self._extra.as_ref()
84    }
85
86    pub fn try_from_t_with_string(query: &Query<String>) -> Result<Self, ScopeFromStrError> {
87        let scope = if let Some(x) = &query.scope {
88            Some(ScopeParameter::<SCOPE>::try_from_t_with_string(x)?)
89        } else {
90            None
91        };
92
93        let mut code_challenge = None;
94        if let Some(cc) = &query.code_challenge {
95            if let Some(ccm) = &query.code_challenge_method {
96                code_challenge = Some((cc.to_owned(), ccm.to_owned()));
97            }
98        };
99
100        let mut this = Self::internal_new(
101            query.client_id.to_owned(),
102            query.redirect_uri.to_owned(),
103            scope,
104            query.state.to_owned(),
105            code_challenge,
106            query.nonce.to_owned(),
107        );
108        if let Some(extra) = query.extra() {
109            this.set_extra(extra.to_owned());
110        }
111        Ok(this)
112    }
113}
114
115impl<SCOPE> From<&Query<SCOPE>> for Query<String>
116where
117    SCOPE: Scope,
118{
119    fn from(query: &Query<SCOPE>) -> Self {
120        let mut code_challenge = None;
121        if let Some(cc) = &query.code_challenge {
122            if let Some(ccm) = &query.code_challenge_method {
123                code_challenge = Some((cc.to_owned(), ccm.to_owned()));
124            }
125        };
126
127        let mut this = Self::internal_new(
128            query.client_id.to_owned(),
129            query.redirect_uri.to_owned(),
130            query
131                .scope
132                .to_owned()
133                .map(|x| ScopeParameter::<String>::from(&x)),
134            query.state.to_owned(),
135            code_challenge,
136            query.nonce.to_owned(),
137        );
138        if let Some(extra) = query.extra() {
139            this.set_extra(extra.to_owned());
140        }
141        this
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_ser_de() {
151        let query = Query::new(
152            "your_client_id".to_owned(),
153            Some("https://client.example.com/cb".parse().unwrap()),
154            Some(vec!["email".to_owned(), "profile".to_owned()].into()),
155            Some("STATE".to_owned()),
156        );
157        match serde_qs::to_string(&query) {
158            Ok(query_str) => {
159                assert_eq!(query_str, "response_type=code&client_id=your_client_id&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=email+profile&state=STATE");
160            }
161            Err(err) => panic!("{err}"),
162        }
163    }
164}