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}