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#[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 pub fn add_scope(mut self, scope: Scope) -> Self {
82 self.scopes.push(Cow::Owned(scope));
83 self
84 }
85
86 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 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 pub fn use_implicit_flow(mut self) -> Self {
119 self.response_type = "token".into();
120 self
121 }
122
123 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 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 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 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 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}