graph_oauth/identity/credentials/legacy/
implicit_credential.rs1use crate::identity::credentials::app_config::AppConfig;
2use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType};
3use crate::oauth_serializer::{AuthParameter, AuthSerializer};
4use graph_core::crypto::secure_random_32;
5use graph_error::{AuthorizationFailure, IdentityResult, AF};
6use http::{HeaderMap, HeaderName, HeaderValue};
7use reqwest::IntoUrl;
8use std::collections::HashMap;
9use url::Url;
10
11credential_builder_base!(ImplicitCredentialBuilder);
12
13#[derive(Clone)]
19pub struct ImplicitCredential {
20 pub(crate) app_config: AppConfig,
21 pub(crate) response_type: Vec<ResponseType>,
31 pub(crate) response_mode: ResponseMode,
45 pub(crate) state: Option<String>,
52 pub(crate) nonce: String,
58 pub(crate) prompt: Option<Prompt>,
69 pub(crate) login_hint: Option<String>,
75 pub(crate) domain_hint: Option<String>,
83}
84
85impl ImplicitCredential {
86 pub fn new<U: ToString, I: IntoIterator<Item = U>>(
87 client_id: impl AsRef<str>,
88 scope: I,
89 ) -> ImplicitCredential {
90 ImplicitCredential {
91 app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(),
92 response_type: vec![ResponseType::Code],
93 response_mode: ResponseMode::Query,
94 state: None,
95 nonce: secure_random_32(),
96 prompt: None,
97 login_hint: None,
98 domain_hint: None,
99 }
100 }
101
102 pub fn builder(client_id: impl AsRef<str>) -> ImplicitCredentialBuilder {
103 ImplicitCredentialBuilder::new(client_id)
104 }
105
106 pub fn url(&self) -> IdentityResult<Url> {
107 self.url_with_host(&AzureCloudInstance::default())
108 }
109
110 pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> {
111 let mut serializer = AuthSerializer::new();
112 let client_id = self.app_config.client_id.to_string();
113 if client_id.is_empty() || self.app_config.client_id.is_nil() {
114 return AuthorizationFailure::result("client_id");
115 }
116
117 if self.nonce.trim().is_empty() {
118 return AuthorizationFailure::result("nonce");
119 }
120
121 serializer
122 .client_id(client_id.as_str())
123 .nonce(self.nonce.as_str())
124 .set_scope(self.app_config.scope.clone());
125
126 let response_types: Vec<String> =
127 self.response_type.iter().map(|s| s.to_string()).collect();
128
129 if response_types.is_empty() {
130 serializer.response_type(ResponseType::Code);
131 serializer.response_mode(self.response_mode.as_ref());
132 } else {
133 let response_type = response_types.join(" ").trim().to_owned();
134 if response_type.is_empty() {
135 serializer.response_type(ResponseType::Code);
136 } else {
137 serializer.response_type(response_type);
138 }
139
140 if self.response_type.contains(&ResponseType::IdToken) {
141 if self.response_mode.eq(&ResponseMode::Query) {
146 return Err(AF::msg_err(
147 "response_mode",
148 "ResponseType::IdToken requires ResponseMode::Fragment or ResponseMode::FormPost")
149 );
150 } else {
151 serializer.response_mode(self.response_mode.as_ref());
152 }
153 } else {
154 serializer.response_mode(self.response_mode.as_ref());
155 }
156 }
157
158 if self.app_config.scope.is_empty() {
160 return Err(AF::required("scope"));
161 }
162
163 if let Some(state) = self.state.as_ref() {
164 serializer.state(state.as_str());
165 }
166
167 if let Some(prompt) = self.prompt.as_ref() {
168 serializer.prompt(prompt.as_ref());
169 }
170
171 if let Some(domain_hint) = self.domain_hint.as_ref() {
172 serializer.domain_hint(domain_hint.as_str());
173 }
174
175 if let Some(login_hint) = self.login_hint.as_ref() {
176 serializer.login_hint(login_hint.as_str());
177 }
178
179 let query = serializer.encode_query(
180 vec![
181 AuthParameter::RedirectUri,
182 AuthParameter::ResponseMode,
183 AuthParameter::State,
184 AuthParameter::Prompt,
185 AuthParameter::LoginHint,
186 AuthParameter::DomainHint,
187 ],
188 vec![
189 AuthParameter::ClientId,
190 AuthParameter::ResponseType,
191 AuthParameter::Scope,
192 AuthParameter::Nonce,
193 ],
194 )?;
195
196 let mut uri = azure_cloud_instance.auth_uri(&self.app_config.authority)?;
197 uri.set_query(Some(query.as_str()));
198 Ok(uri)
199 }
200}
201
202#[derive(Clone)]
203pub struct ImplicitCredentialBuilder {
204 credential: ImplicitCredential,
205}
206
207impl ImplicitCredentialBuilder {
208 pub fn new(client_id: impl AsRef<str>) -> ImplicitCredentialBuilder {
209 ImplicitCredentialBuilder {
210 credential: ImplicitCredential {
211 app_config: AppConfig::new(client_id.as_ref()),
212 response_type: vec![ResponseType::Code],
213 response_mode: ResponseMode::Query,
214 state: None,
215 nonce: secure_random_32(),
216 prompt: None,
217 login_hint: None,
218 domain_hint: None,
219 },
220 }
221 }
222
223 pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> {
224 self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?);
225 Ok(self)
226 }
227
228 pub fn with_response_type<I: IntoIterator<Item = ResponseType>>(
231 &mut self,
232 response_type: I,
233 ) -> &mut Self {
234 self.credential.response_type = response_type.into_iter().collect();
235 self
236 }
237
238 pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self {
250 self.credential.response_mode = response_mode;
251 self
252 }
253
254 pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self {
261 self.credential.nonce = nonce.as_ref().to_owned();
262 self
263 }
264
265 pub fn with_generated_nonce(&mut self) -> &mut Self {
271 self.credential.nonce = secure_random_32();
272 self
273 }
274
275 pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self {
276 self.credential.state = Some(state.as_ref().to_owned());
277 self
278 }
279
280 pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self {
291 self.credential.prompt = Some(prompt);
292 self
293 }
294
295 pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self {
296 self.credential.domain_hint = Some(domain_hint.as_ref().to_owned());
297 self
298 }
299
300 pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self {
301 self.credential.login_hint = Some(login_hint.as_ref().to_owned());
302 self
303 }
304
305 pub fn url(&self) -> IdentityResult<Url> {
306 self.credential.url()
307 }
308
309 pub fn build(&self) -> ImplicitCredential {
310 self.credential.clone()
311 }
312}
313
314#[cfg(test)]
315mod test {
316 use super::*;
317 use uuid::Uuid;
318
319 #[test]
320 fn serialize_uri() {
321 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
322 .with_response_type(vec![ResponseType::Token])
323 .with_redirect_uri("https://localhost/myapp")
324 .unwrap()
325 .with_scope(["User.Read"])
326 .with_response_mode(ResponseMode::Fragment)
327 .with_state("12345")
328 .with_nonce("678910")
329 .with_prompt(Prompt::None)
330 .with_login_hint("myuser@mycompany.com")
331 .build();
332
333 let url_result = authorizer.url();
334 assert!(url_result.is_ok());
335 }
336
337 #[test]
338 fn set_open_id_fragment() {
339 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
340 .with_response_type(vec![ResponseType::IdToken])
341 .with_response_mode(ResponseMode::Fragment)
342 .with_redirect_uri("https://localhost:8080/myapp")
343 .unwrap()
344 .with_scope(["User.Read"])
345 .with_nonce("678910")
346 .build();
347
348 let url_result = authorizer.url();
349 assert!(url_result.is_ok());
350 let url = url_result.unwrap();
351 let url_str = url.as_str();
352 assert!(url_str.contains("response_mode=fragment"))
353 }
354
355 #[test]
356 fn set_response_mode_fragment() {
357 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
358 .with_response_mode(ResponseMode::Fragment)
359 .with_redirect_uri("https://localhost:8080/myapp")
360 .unwrap()
361 .with_scope(["User.Read"])
362 .with_nonce("678910")
363 .build();
364
365 let url_result = authorizer.url();
366 assert!(url_result.is_ok());
367 let url = url_result.unwrap();
368 let url_str = url.as_str();
369 assert!(url_str.contains("response_mode=fragment"))
370 }
371
372 #[test]
373 fn response_type_id_token_token_serializes() {
374 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
375 .with_response_type(vec![ResponseType::IdToken, ResponseType::Token])
376 .with_response_mode(ResponseMode::Fragment)
377 .with_redirect_uri("http://localhost:8080/myapp")
378 .unwrap()
379 .with_scope(["User.Read"])
380 .with_nonce("678910")
381 .build();
382
383 let url_result = authorizer.url();
384 assert!(url_result.is_ok());
385 let url = url_result.unwrap();
386 let url_str = url.as_str();
387 assert!(url_str.contains("response_mode=fragment"));
388 assert!(url_str.contains("response_type=id_token+token"));
389 }
390
391 #[test]
392 fn response_type_id_token_token_serializes_from_string() {
393 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
394 .with_response_type(ResponseType::StringSet(
395 vec!["id_token".to_owned(), "token".to_owned()]
396 .into_iter()
397 .collect(),
398 ))
399 .with_response_mode(ResponseMode::FormPost)
400 .with_redirect_uri("http://localhost:8080/myapp")
401 .unwrap()
402 .with_scope(["User.Read"])
403 .with_nonce("678910")
404 .build();
405
406 let url_result = authorizer.url();
407 assert!(url_result.is_ok());
408 let url = url_result.unwrap();
409 let url_str = url.as_str();
410 assert!(url_str.contains("response_mode=form_post"));
411 assert!(url_str.contains("response_type=id_token+token"))
412 }
413
414 #[test]
415 #[should_panic]
416 fn response_type_id_token_panics_with_response_mode_query() {
417 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
418 .with_response_type(ResponseType::IdToken)
419 .with_redirect_uri("http://localhost:8080/myapp")
420 .unwrap()
421 .with_scope(["User.Read"])
422 .with_nonce("678910")
423 .build();
424
425 let url = authorizer.url().unwrap();
426 let url_str = url.as_str();
427 assert!(url_str.contains("response_type=id_token"))
428 }
429
430 #[test]
431 #[should_panic]
432 fn missing_scope_panic() {
433 let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
434 .with_response_type(vec![ResponseType::Token])
435 .with_redirect_uri("https://example.com/myapp")
436 .unwrap()
437 .with_nonce("678910")
438 .build();
439
440 let _ = authorizer.url().unwrap();
441 }
442
443 #[test]
444 fn generate_nonce() {
445 let url = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e")
446 .with_redirect_uri("http://localhost:8080")
447 .unwrap()
448 .with_client_id(Uuid::new_v4())
449 .with_scope(["read", "write"])
450 .with_response_type(vec![ResponseType::Code, ResponseType::IdToken])
451 .with_response_mode(ResponseMode::Fragment)
452 .url()
453 .unwrap();
454
455 let query = url.query().unwrap();
456 assert!(query.contains("response_mode=fragment"));
457 assert!(query.contains("response_type=code+id_token"));
458 assert!(query.contains("nonce"));
459 }
460}