graph_oauth/identity/
device_authorization_response.rs

1use std::collections::{BTreeSet, HashMap};
2use std::fmt::{Display, Formatter};
3use std::str::FromStr;
4
5use serde_json::Value;
6
7#[cfg(feature = "interactive-auth")]
8use graph_core::http::JsonHttpResponse;
9
10#[cfg(feature = "interactive-auth")]
11use crate::interactive::WindowCloseReason;
12
13#[cfg(feature = "interactive-auth")]
14use crate::identity::{DeviceCodeCredential, PublicClientApplication};
15
16/// The Device Authorization Response: the authorization server generates a unique device
17/// verification code and an end-user code that are valid for a limited time and includes
18/// them in the HTTP response body using the "application/json" format [RFC8259] with a
19/// 200 (OK) status code
20///
21/// The actual [device code response](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code#device-authorization-response)
22/// that is received from Microsoft Graph does not include the verification_uri_complete field
23/// even though it's in the [specification](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2).
24/// The device code response from Microsoft Graph looks like similar to the following:
25///
26/// ```json
27/// {
28///     "device_code": String("FABABAAEAAAD--DLA3VO7QrddgJg7WevrgJ7Czy_TDsDClt2ELoEC8ePWFs"),
29///     "expires_in": Number(900),
30///     "interval": Number(5),
31///     "message": String("To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code FQK5HW3UF to authenticate."),
32///     "user_code": String("FQK5HW3UF"),
33///     "verification_uri": String("https://microsoft.com/devicelogin"),
34/// }
35/// ```
36#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
37pub struct DeviceAuthorizationResponse {
38    ///  A long string used to verify the session between the client and the authorization server.
39    /// The client uses this parameter to request the access token from the authorization server.
40    pub device_code: String,
41    /// The number of seconds before the device_code and user_code expire.
42    pub expires_in: u64,
43    /// OPTIONAL
44    /// The minimum amount of time in seconds that the client
45    /// SHOULD wait between polling requests to the token endpoint.  If no
46    /// value is provided, clients MUST use 5 as the default.
47    #[serde(default = "default_interval")]
48    pub interval: u64,
49    ///  User friendly text response that can be used for display purpose.
50    pub message: String,
51    /// A short string shown to the user that's used to identify the session on a secondary device.
52    pub user_code: String,
53    /// Verification URL where the user must navigate to authenticate using the device code
54    /// and credentials.
55    pub verification_uri: String,
56    /// The verification_uri_complete response field is not included or supported
57    /// by Microsoft at this time. It is included here because it is part of the
58    /// [standard](https://datatracker.ietf.org/doc/html/rfc8628) and in the case
59    /// that Microsoft decides to include it.
60    pub verification_uri_complete: Option<String>,
61    /// List of the scopes that would be held by token.
62    pub scopes: Option<BTreeSet<String>>,
63    #[serde(flatten)]
64    pub additional_fields: HashMap<String, Value>,
65}
66
67fn default_interval() -> u64 {
68    5
69}
70
71impl Display for DeviceAuthorizationResponse {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        write!(
74            f,
75            "{}, {}, {}, {}, {}, {}, {:#?}, {:#?}",
76            self.device_code,
77            self.expires_in,
78            self.interval,
79            self.message,
80            self.user_code,
81            self.verification_uri,
82            self.verification_uri_complete,
83            self.scopes
84        )
85    }
86}
87
88/// Response types used when polling for a device code
89/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
90#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
91pub enum PollDeviceCodeEvent {
92    /// The user hasn't finished authenticating, but hasn't canceled the flow.
93    /// Repeat the request after at least interval seconds.
94    AuthorizationPending,
95    /// The end user denied the authorization request.
96    /// Stop polling and revert to an unauthenticated state.
97    AuthorizationDeclined,
98    /// The device_code sent to the /token endpoint wasn't recognized.
99    /// Verify that the client is sending the correct device_code in the request.
100    BadVerificationCode,
101    /// Value of expires_in has been exceeded and authentication is no longer possible
102    /// with device_code.
103    /// Stop polling and revert to an unauthenticated state.
104    ExpiredToken,
105
106    /// Not yet supported by Microsoft but listed in the specification.
107    ///
108    /// The authorization request was denied.
109    AccessDenied,
110
111    /// Not yet supported by Microsoft but listed in the specification.
112    ///
113    /// A variant of "authorization_pending", the authorization request is
114    /// still pending and polling should continue, but the interval MUST
115    /// be increased by 5 seconds for this and all subsequent requests.
116    SlowDown,
117}
118
119impl PollDeviceCodeEvent {
120    pub fn as_str(&self) -> &'static str {
121        match self {
122            PollDeviceCodeEvent::AuthorizationPending => "authorization_pending",
123            PollDeviceCodeEvent::AuthorizationDeclined => "authorization_declined",
124            PollDeviceCodeEvent::BadVerificationCode => "bad_verification_code",
125            PollDeviceCodeEvent::ExpiredToken => "expired_token",
126            PollDeviceCodeEvent::AccessDenied => "access_denied",
127            PollDeviceCodeEvent::SlowDown => "slow_down",
128        }
129    }
130}
131
132impl FromStr for PollDeviceCodeEvent {
133    type Err = ();
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        match s {
137            "authorization_pending" => Ok(PollDeviceCodeEvent::AuthorizationPending),
138            "authorization_declined" => Ok(PollDeviceCodeEvent::AuthorizationDeclined),
139            "bad_verification_code" => Ok(PollDeviceCodeEvent::BadVerificationCode),
140            "expired_token" => Ok(PollDeviceCodeEvent::ExpiredToken),
141            "access_denied" => Ok(PollDeviceCodeEvent::AccessDenied),
142            "slow_down" => Ok(PollDeviceCodeEvent::SlowDown),
143            _ => Err(()),
144        }
145    }
146}
147
148impl AsRef<str> for PollDeviceCodeEvent {
149    fn as_ref(&self) -> &str {
150        self.as_str()
151    }
152}
153
154impl Display for PollDeviceCodeEvent {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        write!(f, "{}", self.as_str())
157    }
158}
159
160#[cfg(feature = "interactive-auth")]
161#[derive(Debug)]
162pub enum InteractiveDeviceCodeEvent {
163    DeviceAuthorizationResponse {
164        response: JsonHttpResponse,
165        device_authorization_response: Option<DeviceAuthorizationResponse>,
166    },
167    PollDeviceCode {
168        poll_device_code_event: PollDeviceCodeEvent,
169        response: JsonHttpResponse,
170    },
171    WindowClosed(WindowCloseReason),
172    SuccessfulAuthEvent {
173        response: JsonHttpResponse,
174        public_application: PublicClientApplication<DeviceCodeCredential>,
175    },
176}