oauth2/
code.rs

1use crate::{
2    AuthUrl, Client, ClientId, CsrfToken, EndpointState, ErrorResponse, PkceCodeChallenge,
3    RedirectUrl, ResponseType, RevocableToken, Scope, TokenIntrospectionResponse, TokenResponse,
4};
5
6use url::Url;
7
8use std::borrow::Cow;
9
10impl<
11        TE,
12        TR,
13        TIR,
14        RT,
15        TRE,
16        HasAuthUrl,
17        HasDeviceAuthUrl,
18        HasIntrospectionUrl,
19        HasRevocationUrl,
20        HasTokenUrl,
21    >
22    Client<
23        TE,
24        TR,
25        TIR,
26        RT,
27        TRE,
28        HasAuthUrl,
29        HasDeviceAuthUrl,
30        HasIntrospectionUrl,
31        HasRevocationUrl,
32        HasTokenUrl,
33    >
34where
35    TE: ErrorResponse + 'static,
36    TR: TokenResponse,
37    TIR: TokenIntrospectionResponse,
38    RT: RevocableToken,
39    TRE: ErrorResponse + 'static,
40    HasAuthUrl: EndpointState,
41    HasDeviceAuthUrl: EndpointState,
42    HasIntrospectionUrl: EndpointState,
43    HasRevocationUrl: EndpointState,
44    HasTokenUrl: EndpointState,
45{
46    pub(crate) fn authorize_url_impl<'a, S>(
47        &'a self,
48        auth_url: &'a AuthUrl,
49        state_fn: S,
50    ) -> AuthorizationRequest<'a>
51    where
52        S: FnOnce() -> CsrfToken,
53    {
54        AuthorizationRequest {
55            auth_url,
56            client_id: &self.client_id,
57            extra_params: Vec::new(),
58            pkce_challenge: None,
59            redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed),
60            response_type: "code".into(),
61            scopes: Vec::new(),
62            state: state_fn(),
63        }
64    }
65}
66
67/// A request to the authorization endpoint
68#[derive(Debug)]
69pub struct AuthorizationRequest<'a> {
70    pub(crate) auth_url: &'a AuthUrl,
71    pub(crate) client_id: &'a ClientId,
72    pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
73    pub(crate) pkce_challenge: Option<PkceCodeChallenge>,
74    pub(crate) redirect_url: Option<Cow<'a, RedirectUrl>>,
75    pub(crate) response_type: Cow<'a, str>,
76    pub(crate) scopes: Vec<Cow<'a, Scope>>,
77    pub(crate) state: CsrfToken,
78}
79impl<'a> AuthorizationRequest<'a> {
80    /// Appends a new scope to the authorization URL.
81    pub fn add_scope(mut self, scope: Scope) -> Self {
82        self.scopes.push(Cow::Owned(scope));
83        self
84    }
85
86    /// Appends a collection of scopes to the token request.
87    pub fn add_scopes<I>(mut self, scopes: I) -> Self
88    where
89        I: IntoIterator<Item = Scope>,
90    {
91        self.scopes.extend(scopes.into_iter().map(Cow::Owned));
92        self
93    }
94
95    /// Appends an extra param to the authorization URL.
96    ///
97    /// This method allows extensions to be used without direct support from
98    /// this crate. If `name` conflicts with a parameter managed by this crate, the
99    /// behavior is undefined. In particular, do not set parameters defined by
100    /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or
101    /// [RFC 7636](https://tools.ietf.org/html/rfc7636).
102    ///
103    /// # Security Warning
104    ///
105    /// Callers should follow the security recommendations for any OAuth2 extensions used with
106    /// this function, which are beyond the scope of
107    /// [RFC 6749](https://tools.ietf.org/html/rfc6749).
108    pub fn add_extra_param<N, V>(mut self, name: N, value: V) -> Self
109    where
110        N: Into<Cow<'a, str>>,
111        V: Into<Cow<'a, str>>,
112    {
113        self.extra_params.push((name.into(), value.into()));
114        self
115    }
116
117    /// Enables the [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2) flow.
118    pub fn use_implicit_flow(mut self) -> Self {
119        self.response_type = "token".into();
120        self
121    }
122
123    /// Enables custom flows other than the `code` and `token` (implicit flow) grant.
124    pub fn set_response_type(mut self, response_type: &ResponseType) -> Self {
125        self.response_type = (**response_type).to_owned().into();
126        self
127    }
128
129    /// Enables the use of [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636)
130    /// (PKCE).
131    ///
132    /// PKCE is *highly recommended* for all public clients (i.e., those for which there
133    /// is no client secret or for which the client secret is distributed with the client,
134    /// such as in a native, mobile app, or browser app).
135    pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self {
136        self.pkce_challenge = Some(pkce_code_challenge);
137        self
138    }
139
140    /// Overrides the `redirect_url` to the one specified.
141    pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self {
142        self.redirect_url = Some(redirect_url);
143        self
144    }
145
146    /// Returns the full authorization URL and CSRF state for this authorization
147    /// request.
148    pub fn url(self) -> (Url, CsrfToken) {
149        let scopes = self
150            .scopes
151            .iter()
152            .map(|s| s.to_string())
153            .collect::<Vec<_>>()
154            .join(" ");
155
156        let url = {
157            let mut pairs: Vec<(&str, &str)> = vec![
158                ("response_type", self.response_type.as_ref()),
159                ("client_id", self.client_id),
160                ("state", self.state.secret()),
161            ];
162
163            if let Some(ref pkce_challenge) = self.pkce_challenge {
164                pairs.push(("code_challenge", pkce_challenge.as_str()));
165                pairs.push(("code_challenge_method", pkce_challenge.method().as_str()));
166            }
167
168            if let Some(ref redirect_url) = self.redirect_url {
169                pairs.push(("redirect_uri", redirect_url.as_str()));
170            }
171
172            if !scopes.is_empty() {
173                pairs.push(("scope", &scopes));
174            }
175
176            let mut url: Url = self.auth_url.url().to_owned();
177
178            url.query_pairs_mut()
179                .extend_pairs(pairs.iter().map(|&(k, v)| (k, v)));
180
181            url.query_pairs_mut()
182                .extend_pairs(self.extra_params.iter().cloned());
183            url
184        };
185
186        (url, self.state)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use crate::basic::BasicClient;
193    use crate::tests::new_client;
194    use crate::{
195        AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier,
196        RedirectUrl, ResponseType, Scope, TokenUrl,
197    };
198
199    use url::form_urlencoded::byte_serialize;
200    use url::Url;
201
202    use std::borrow::Cow;
203
204    #[test]
205    fn test_authorize_url() {
206        let client = new_client();
207        let (url, _) = client
208            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
209            .url();
210
211        assert_eq!(
212            Url::parse(
213                "https://example.com/auth?response_type=code&client_id=aaa&state=csrf_token"
214            )
215            .unwrap(),
216            url
217        );
218    }
219
220    #[test]
221    fn test_authorize_random() {
222        let client = new_client();
223        let (url, csrf_state) = client.authorize_url(CsrfToken::new_random).url();
224
225        assert_eq!(
226            Url::parse(&format!(
227                "https://example.com/auth?response_type=code&client_id=aaa&state={}",
228                byte_serialize(csrf_state.secret().clone().into_bytes().as_slice())
229                    .collect::<Vec<_>>()
230                    .join("")
231            ))
232            .unwrap(),
233            url
234        );
235    }
236
237    #[test]
238    fn test_authorize_url_pkce() {
239        // Example from https://tools.ietf.org/html/rfc7636#appendix-B
240        let client = new_client();
241
242        let (url, _) = client
243            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
244            .set_pkce_challenge(PkceCodeChallenge::from_code_verifier_sha256(
245                &PkceCodeVerifier::new("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string()),
246            ))
247            .url();
248        assert_eq!(
249            Url::parse(concat!(
250                "https://example.com/auth",
251                "?response_type=code&client_id=aaa",
252                "&state=csrf_token",
253                "&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
254                "&code_challenge_method=S256",
255            ))
256            .unwrap(),
257            url
258        );
259    }
260
261    #[test]
262    fn test_authorize_url_implicit() {
263        let client = new_client();
264
265        let (url, _) = client
266            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
267            .use_implicit_flow()
268            .url();
269
270        assert_eq!(
271            Url::parse(
272                "https://example.com/auth?response_type=token&client_id=aaa&state=csrf_token"
273            )
274            .unwrap(),
275            url
276        );
277    }
278
279    #[test]
280    fn test_authorize_url_with_param() {
281        let client = BasicClient::new(ClientId::new("aaa".to_string()))
282            .set_client_secret(ClientSecret::new("bbb".to_string()))
283            .set_auth_uri(AuthUrl::new("https://example.com/auth?foo=bar".to_string()).unwrap())
284            .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap());
285
286        let (url, _) = client
287            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
288            .url();
289
290        assert_eq!(
291            Url::parse(
292                "https://example.com/auth?foo=bar&response_type=code&client_id=aaa&state=csrf_token"
293            )
294              .unwrap(),
295            url
296        );
297    }
298
299    #[test]
300    fn test_authorize_url_with_scopes() {
301        let scopes = vec![
302            Scope::new("read".to_string()),
303            Scope::new("write".to_string()),
304        ];
305        let (url, _) = new_client()
306            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
307            .add_scopes(scopes)
308            .url();
309
310        assert_eq!(
311            Url::parse(
312                "https://example.com/auth\
313             ?response_type=code\
314             &client_id=aaa\
315             &state=csrf_token\
316             &scope=read+write"
317            )
318            .unwrap(),
319            url
320        );
321    }
322
323    #[test]
324    fn test_authorize_url_with_one_scope() {
325        let (url, _) = new_client()
326            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
327            .add_scope(Scope::new("read".to_string()))
328            .url();
329
330        assert_eq!(
331            Url::parse(
332                "https://example.com/auth\
333             ?response_type=code\
334             &client_id=aaa\
335             &state=csrf_token\
336             &scope=read"
337            )
338            .unwrap(),
339            url
340        );
341    }
342
343    #[test]
344    fn test_authorize_url_with_extension_response_type() {
345        let client = new_client();
346
347        let (url, _) = client
348            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
349            .set_response_type(&ResponseType::new("code token".to_string()))
350            .add_extra_param("foo", "bar")
351            .url();
352
353        assert_eq!(
354            Url::parse(
355                "https://example.com/auth?response_type=code+token&client_id=aaa&state=csrf_token\
356             &foo=bar"
357            )
358            .unwrap(),
359            url
360        );
361    }
362
363    #[test]
364    fn test_authorize_url_with_redirect_url() {
365        let client = new_client()
366            .set_redirect_uri(RedirectUrl::new("https://localhost/redirect".to_string()).unwrap());
367
368        let (url, _) = client
369            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
370            .url();
371
372        assert_eq!(
373            Url::parse(
374                "https://example.com/auth?response_type=code\
375             &client_id=aaa\
376             &state=csrf_token\
377             &redirect_uri=https%3A%2F%2Flocalhost%2Fredirect"
378            )
379            .unwrap(),
380            url
381        );
382    }
383
384    #[test]
385    fn test_authorize_url_with_redirect_url_override() {
386        let client = new_client()
387            .set_redirect_uri(RedirectUrl::new("https://localhost/redirect".to_string()).unwrap());
388
389        let (url, _) = client
390            .authorize_url(|| CsrfToken::new("csrf_token".to_string()))
391            .set_redirect_uri(Cow::Owned(
392                RedirectUrl::new("https://localhost/alternative".to_string()).unwrap(),
393            ))
394            .url();
395
396        assert_eq!(
397            Url::parse(
398                "https://example.com/auth?response_type=code\
399             &client_id=aaa\
400             &state=csrf_token\
401             &redirect_uri=https%3A%2F%2Flocalhost%2Falternative"
402            )
403            .unwrap(),
404            url
405        );
406    }
407}