mas_data_model/oauth2/
device_code_grant.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::net::IpAddr;
8
9use chrono::{DateTime, Utc};
10use oauth2_types::scope::Scope;
11use serde::Serialize;
12use ulid::Ulid;
13
14use crate::{BrowserSession, InvalidTransitionError, Session, UserAgent};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "snake_case", tag = "state")]
18pub enum DeviceCodeGrantState {
19    /// The device code grant is pending.
20    Pending,
21
22    /// The device code grant has been fulfilled by a user.
23    Fulfilled {
24        /// The browser session which was used to complete this device code
25        /// grant.
26        browser_session_id: Ulid,
27
28        /// The time at which this device code grant was fulfilled.
29        fulfilled_at: DateTime<Utc>,
30    },
31
32    /// The device code grant has been rejected by a user.
33    Rejected {
34        /// The browser session which was used to reject this device code grant.
35        browser_session_id: Ulid,
36
37        /// The time at which this device code grant was rejected.
38        rejected_at: DateTime<Utc>,
39    },
40
41    /// The device code grant was exchanged for an access token.
42    Exchanged {
43        /// The browser session which was used to exchange this device code
44        /// grant.
45        browser_session_id: Ulid,
46
47        /// The time at which the device code grant was fulfilled.
48        fulfilled_at: DateTime<Utc>,
49
50        /// The time at which this device code grant was exchanged.
51        exchanged_at: DateTime<Utc>,
52
53        /// The OAuth 2.0 session ID which was created by this device code
54        /// grant.
55        session_id: Ulid,
56    },
57}
58
59impl DeviceCodeGrantState {
60    /// Mark this device code grant as fulfilled, returning a new state.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the device code grant is not in the [`Pending`]
65    /// state.
66    ///
67    /// [`Pending`]: DeviceCodeGrantState::Pending
68    pub fn fulfill(
69        self,
70        browser_session: &BrowserSession,
71        fulfilled_at: DateTime<Utc>,
72    ) -> Result<Self, InvalidTransitionError> {
73        match self {
74            DeviceCodeGrantState::Pending => Ok(DeviceCodeGrantState::Fulfilled {
75                browser_session_id: browser_session.id,
76                fulfilled_at,
77            }),
78            _ => Err(InvalidTransitionError),
79        }
80    }
81
82    /// Mark this device code grant as rejected, returning a new state.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the device code grant is not in the [`Pending`]
87    /// state.
88    ///
89    /// [`Pending`]: DeviceCodeGrantState::Pending
90    pub fn reject(
91        self,
92        browser_session: &BrowserSession,
93        rejected_at: DateTime<Utc>,
94    ) -> Result<Self, InvalidTransitionError> {
95        match self {
96            DeviceCodeGrantState::Pending => Ok(DeviceCodeGrantState::Rejected {
97                browser_session_id: browser_session.id,
98                rejected_at,
99            }),
100            _ => Err(InvalidTransitionError),
101        }
102    }
103
104    /// Mark this device code grant as exchanged, returning a new state.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the device code grant is not in the [`Fulfilled`]
109    /// state.
110    ///
111    /// [`Fulfilled`]: DeviceCodeGrantState::Fulfilled
112    pub fn exchange(
113        self,
114        session: &Session,
115        exchanged_at: DateTime<Utc>,
116    ) -> Result<Self, InvalidTransitionError> {
117        match self {
118            DeviceCodeGrantState::Fulfilled {
119                fulfilled_at,
120                browser_session_id,
121                ..
122            } => Ok(DeviceCodeGrantState::Exchanged {
123                browser_session_id,
124                fulfilled_at,
125                exchanged_at,
126                session_id: session.id,
127            }),
128            _ => Err(InvalidTransitionError),
129        }
130    }
131
132    /// Returns `true` if the device code grant state is [`Pending`].
133    ///
134    /// [`Pending`]: DeviceCodeGrantState::Pending
135    #[must_use]
136    pub fn is_pending(&self) -> bool {
137        matches!(self, Self::Pending)
138    }
139
140    /// Returns `true` if the device code grant state is [`Fulfilled`].
141    ///
142    /// [`Fulfilled`]: DeviceCodeGrantState::Fulfilled
143    #[must_use]
144    pub fn is_fulfilled(&self) -> bool {
145        matches!(self, Self::Fulfilled { .. })
146    }
147
148    /// Returns `true` if the device code grant state is [`Rejected`].
149    ///
150    /// [`Rejected`]: DeviceCodeGrantState::Rejected
151    #[must_use]
152    pub fn is_rejected(&self) -> bool {
153        matches!(self, Self::Rejected { .. })
154    }
155
156    /// Returns `true` if the device code grant state is [`Exchanged`].
157    ///
158    /// [`Exchanged`]: DeviceCodeGrantState::Exchanged
159    #[must_use]
160    pub fn is_exchanged(&self) -> bool {
161        matches!(self, Self::Exchanged { .. })
162    }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
166pub struct DeviceCodeGrant {
167    pub id: Ulid,
168    #[serde(flatten)]
169    pub state: DeviceCodeGrantState,
170
171    /// The client ID which requested this device code grant.
172    pub client_id: Ulid,
173
174    /// The scope which was requested by this device code grant.
175    pub scope: Scope,
176
177    /// The user code which was generated for this device code grant.
178    /// This is the one that the user will enter into their client.
179    pub user_code: String,
180
181    /// The device code which was generated for this device code grant.
182    /// This is the one that the client will use to poll for an access token.
183    pub device_code: String,
184
185    /// The time at which this device code grant was created.
186    pub created_at: DateTime<Utc>,
187
188    /// The time at which this device code grant will expire.
189    pub expires_at: DateTime<Utc>,
190
191    /// The IP address of the client which requested this device code grant.
192    pub ip_address: Option<IpAddr>,
193
194    /// The user agent used to request this device code grant.
195    pub user_agent: Option<UserAgent>,
196}
197
198impl std::ops::Deref for DeviceCodeGrant {
199    type Target = DeviceCodeGrantState;
200
201    fn deref(&self) -> &Self::Target {
202        &self.state
203    }
204}
205
206impl DeviceCodeGrant {
207    /// Mark this device code grant as fulfilled, returning the updated grant.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the device code grant is not in the [`Pending`]
212    /// state.
213    ///
214    /// [`Pending`]: DeviceCodeGrantState::Pending
215    pub fn fulfill(
216        self,
217        browser_session: &BrowserSession,
218        fulfilled_at: DateTime<Utc>,
219    ) -> Result<Self, InvalidTransitionError> {
220        Ok(Self {
221            state: self.state.fulfill(browser_session, fulfilled_at)?,
222            ..self
223        })
224    }
225
226    /// Mark this device code grant as rejected, returning the updated grant.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the device code grant is not in the [`Pending`]
231    ///
232    /// [`Pending`]: DeviceCodeGrantState::Pending
233    pub fn reject(
234        self,
235        browser_session: &BrowserSession,
236        rejected_at: DateTime<Utc>,
237    ) -> Result<Self, InvalidTransitionError> {
238        Ok(Self {
239            state: self.state.reject(browser_session, rejected_at)?,
240            ..self
241        })
242    }
243
244    /// Mark this device code grant as exchanged, returning the updated grant.
245    ///
246    /// # Errors
247    ///
248    /// Returns an error if the device code grant is not in the [`Fulfilled`]
249    /// state.
250    ///
251    /// [`Fulfilled`]: DeviceCodeGrantState::Fulfilled
252    pub fn exchange(
253        self,
254        session: &Session,
255        exchanged_at: DateTime<Utc>,
256    ) -> Result<Self, InvalidTransitionError> {
257        Ok(Self {
258            state: self.state.exchange(session, exchanged_at)?,
259            ..self
260        })
261    }
262}