graph_oauth/identity/credentials/legacy/
implicit_credential.rs

1use 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/// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens)
14/// are returned directly from the /authorize endpoint instead of the /token endpoint. This is
15/// often used as part of the authorization code flow, in what is called the "hybrid flow" -
16/// retrieving the ID token on the /authorize request along with an authorization code.
17/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
18#[derive(Clone)]
19pub struct ImplicitCredential {
20    pub(crate) app_config: AppConfig,
21    /// Required
22    /// If not set, defaults to code
23    /// Must include id_token for OpenID Connect sign-in. It may also include the response_type
24    /// token. Using token here will allow your app to receive an access token immediately from
25    /// the authorize endpoint without having to make a second request to the authorize endpoint.
26    /// If you use the token response_type, the scope parameter must contain a scope indicating
27    /// which resource to issue the token for (for example, user.read on Microsoft Graph). It can
28    /// also contain code in place of token to provide an authorization code, for use in the
29    /// authorization code flow. This id_token+code response is sometimes called the hybrid flow.
30    pub(crate) response_type: Vec<ResponseType>,
31    /// Optional (recommended)
32    ///
33    /// Specifies how the identity platform should return the requested token to your app.
34    ///
35    /// Supported values:
36    ///
37    /// - query: Default when requesting an access token. Provides the code as a query string
38    /// parameter on your redirect URI. The query parameter isn't supported when requesting an
39    /// ID token by using the implicit flow.
40    /// - fragment: Default when requesting an ID token by using the implicit flow.
41    /// Also supported if requesting only a code.
42    /// - form_post: Executes a POST containing the code to your redirect URI.
43    /// Supported when requesting a code.
44    pub(crate) response_mode: ResponseMode,
45    /// Optional
46    /// A value included in the request that will also be returned in the token response.
47    /// It can be a string of any content that you wish. A randomly generated unique value is
48    /// typically used for preventing cross-site request forgery attacks. The state is also used
49    /// to encode information about the user's state in the app before the authentication request
50    /// occurred, such as the page or view they were on.
51    pub(crate) state: Option<String>,
52    /// Required
53    ///  A value included in the request, generated by the app, that will be included in the
54    /// resulting id_token as a claim. The app can then verify this value to mitigate token replay
55    /// attacks. The value is typically a randomized, unique string that can be used to identify
56    /// the origin of the request. Only required when an id_token is requested.
57    pub(crate) nonce: String,
58    /// Optional
59    /// Indicates the type of user interaction that is required. The only valid values at this
60    /// time are 'login', 'none', 'select_account', and 'consent'. prompt=login will force the
61    /// user to enter their credentials on that request, negating single-sign on. prompt=none is
62    /// the opposite - it will ensure that the user isn't presented with any interactive prompt
63    /// whatsoever. If the request can't be completed silently via single-sign on, the Microsoft
64    /// identity platform will return an error. prompt=select_account sends the user to an account
65    /// picker where all of the accounts remembered in the session will appear. prompt=consent
66    /// will trigger the OAuth consent dialog after the user signs in, asking the user to grant
67    /// permissions to the app.
68    pub(crate) prompt: Option<Prompt>,
69    /// Optional
70    /// You can use this parameter to pre-fill the username and email address field of the sign-in
71    /// page for the user, if you know the username ahead of time. Often, apps use this parameter
72    /// during re-authentication, after already extracting the login_hint optional claim from an
73    /// earlier sign-in.
74    pub(crate) login_hint: Option<String>,
75    /// Optional
76    /// If included, it will skip the email-based discovery process that user goes through on
77    /// the sign-in page, leading to a slightly more streamlined user experience. This parameter
78    /// is commonly used for Line of Business apps that operate in a single tenant, where they'll
79    /// provide a domain name within a given tenant, forwarding the user to the federation provider
80    /// for that tenant. This hint prevents guests from signing into this application, and limits
81    /// the use of cloud credentials like FIDO.
82    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                // id_token requires fragment or form_post. The Microsoft identity
142                // platform recommends form_post. Unless you explicitly set
143                // fragment then form_post is used here when response type is id_token.
144                // Please file an issue if you encounter related problems.
145                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        // https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc
159        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    /// Default is code. Must include code for the authorization code flow.
229    /// Can also include id_token or token if using the hybrid flow.
230    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    /// Specifies how the identity platform should return the requested token to your app.
239    ///
240    /// Supported values:
241    ///
242    /// - **query**: Default when requesting an access token. Provides the code as a query string
243    ///     parameter on your redirect URI. The query parameter is not supported when requesting an
244    ///     ID token by using the implicit flow.
245    /// - **fragment**: Default when requesting an ID token by using the implicit flow.
246    ///     Also supported if requesting only a code.
247    /// - **form_post**: Executes a POST containing the code to your redirect URI.
248    ///     Supported when requesting a code.
249    pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self {
250        self.credential.response_mode = response_mode;
251        self
252    }
253
254    /// A value included in the request that is included in the resulting id_token as a claim.
255    /// The app can then verify this value to mitigate token replay attacks. The value is
256    /// typically a randomized, unique string that can be used to identify the origin of
257    /// the request.
258    ///
259    /// To have the client generate a nonce for you use [with_nonce_generated](crate::identity::legacy::ImplicitCredentialBuilder::with_generated_nonce)
260    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    /// Generates a secure random nonce.
266    /// A value included in the request, generated by the app, that is included in the
267    /// resulting id_token as a claim. The app can then verify this value to mitigate token
268    /// replay attacks. The value is typically a randomized, unique string that can be used
269    /// to identify the origin of the request.
270    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    /// Indicates the type of user interaction that is required. Valid values are login, none,
281    /// consent, and select_account.
282    ///
283    /// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on.
284    /// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt.
285    ///     If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error.
286    /// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to
287    ///     grant permissions to the app.
288    /// - **prompt=select_account** interrupts single sign-on providing account selection experience
289    ///     listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether.
290    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}