mas_storage/oauth2/device_code_grant.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use std::net::IpAddr;
9
10use async_trait::async_trait;
11use chrono::Duration;
12use mas_data_model::{BrowserSession, Client, Clock, DeviceCodeGrant, Session};
13use oauth2_types::scope::Scope;
14use rand_core::RngCore;
15use ulid::Ulid;
16
17use crate::repository_impl;
18
19/// Parameters used to create a new [`DeviceCodeGrant`]
20pub struct OAuth2DeviceCodeGrantParams<'a> {
21 /// The client which requested the device code grant
22 pub client: &'a Client,
23
24 /// The scope requested by the client
25 pub scope: Scope,
26
27 /// The device code which the client uses to poll for authorisation
28 pub device_code: String,
29
30 /// The user code which the client uses to display to the user
31 pub user_code: String,
32
33 /// After how long the device code expires
34 pub expires_in: Duration,
35
36 /// IP address from which the request was made
37 pub ip_address: Option<IpAddr>,
38
39 /// The user agent from which the request was made
40 pub user_agent: Option<String>,
41}
42
43/// An [`OAuth2DeviceCodeGrantRepository`] helps interacting with
44/// [`DeviceCodeGrant`] saved in the storage backend.
45#[async_trait]
46pub trait OAuth2DeviceCodeGrantRepository: Send + Sync {
47 /// The error type returned by the repository
48 type Error;
49
50 /// Create a new device code grant
51 ///
52 /// Returns the newly created device code grant
53 ///
54 /// # Parameters
55 ///
56 /// * `rng`: A random number generator
57 /// * `clock`: The clock used to generate timestamps
58 /// * `params`: The parameters used to create the device code grant. See the
59 /// fields of [`OAuth2DeviceCodeGrantParams`]
60 ///
61 /// # Errors
62 ///
63 /// Returns [`Self::Error`] if the underlying repository fails
64 async fn add(
65 &mut self,
66 rng: &mut (dyn RngCore + Send),
67 clock: &dyn Clock,
68 params: OAuth2DeviceCodeGrantParams<'_>,
69 ) -> Result<DeviceCodeGrant, Self::Error>;
70
71 /// Lookup a device code grant by its ID
72 ///
73 /// Returns the device code grant if found, [`None`] otherwise
74 ///
75 /// # Parameters
76 ///
77 /// * `id`: The ID of the device code grant
78 ///
79 /// # Errors
80 ///
81 /// Returns [`Self::Error`] if the underlying repository fails
82 async fn lookup(&mut self, id: Ulid) -> Result<Option<DeviceCodeGrant>, Self::Error>;
83
84 /// Lookup a device code grant by its device code
85 ///
86 /// Returns the device code grant if found, [`None`] otherwise
87 ///
88 /// # Parameters
89 ///
90 /// * `device_code`: The device code of the device code grant
91 ///
92 /// # Errors
93 ///
94 /// Returns [`Self::Error`] if the underlying repository fails
95 async fn find_by_device_code(
96 &mut self,
97 device_code: &str,
98 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
99
100 /// Lookup a device code grant by its user code
101 ///
102 /// Returns the device code grant if found, [`None`] otherwise
103 ///
104 /// # Parameters
105 ///
106 /// * `user_code`: The user code of the device code grant
107 ///
108 /// # Errors
109 ///
110 /// Returns [`Self::Error`] if the underlying repository fails
111 async fn find_by_user_code(
112 &mut self,
113 user_code: &str,
114 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
115
116 /// Mark the device code grant as fulfilled with the given browser session
117 ///
118 /// Returns the updated device code grant
119 ///
120 /// # Parameters
121 ///
122 /// * `clock`: The clock used to generate timestamps
123 /// * `device_code_grant`: The device code grant to fulfill
124 /// * `browser_session`: The browser session which was used to fulfill the
125 /// device code grant
126 /// * `locale`: The locale detected from the browser which fulfilled the
127 /// grant, used later to render a human-readable device name
128 ///
129 /// # Errors
130 ///
131 /// Returns [`Self::Error`] if the underlying repository fails or if the
132 /// device code grant is not in the [`Pending`] state
133 ///
134 /// [`Pending`]: mas_data_model::DeviceCodeGrantState::Pending
135 async fn fulfill(
136 &mut self,
137 clock: &dyn Clock,
138 device_code_grant: DeviceCodeGrant,
139 browser_session: &BrowserSession,
140 locale: Option<String>,
141 ) -> Result<DeviceCodeGrant, Self::Error>;
142
143 /// Mark the device code grant as rejected with the given browser session
144 ///
145 /// Returns the updated device code grant
146 ///
147 /// # Parameters
148 ///
149 /// * `clock`: The clock used to generate timestamps
150 /// * `device_code_grant`: The device code grant to reject
151 /// * `browser_session`: The browser session which was used to reject the
152 /// device code grant
153 ///
154 /// # Errors
155 ///
156 /// Returns [`Self::Error`] if the underlying repository fails or if the
157 /// device code grant is not in the [`Pending`] state
158 ///
159 /// [`Pending`]: mas_data_model::DeviceCodeGrantState::Pending
160 async fn reject(
161 &mut self,
162 clock: &dyn Clock,
163 device_code_grant: DeviceCodeGrant,
164 browser_session: &BrowserSession,
165 ) -> Result<DeviceCodeGrant, Self::Error>;
166
167 /// Mark the device code grant as exchanged and store the session which was
168 /// created
169 ///
170 /// Returns the updated device code grant
171 ///
172 /// # Parameters
173 ///
174 /// * `clock`: The clock used to generate timestamps
175 /// * `device_code_grant`: The device code grant to exchange
176 /// * `session`: The OAuth 2.0 session which was created
177 ///
178 /// # Errors
179 ///
180 /// Returns [`Self::Error`] if the underlying repository fails or if the
181 /// device code grant is not in the [`Fulfilled`] state
182 ///
183 /// [`Fulfilled`]: mas_data_model::DeviceCodeGrantState::Fulfilled
184 async fn exchange(
185 &mut self,
186 clock: &dyn Clock,
187 device_code_grant: DeviceCodeGrant,
188 session: &Session,
189 ) -> Result<DeviceCodeGrant, Self::Error>;
190
191 /// Cleanup old device code grants
192 ///
193 /// This will delete device code grants that were created before `until`.
194 /// Uses ULID cursor-based pagination for efficiency.
195 ///
196 /// Returns the number of grants deleted and the cursor for the next batch
197 ///
198 /// # Parameters
199 ///
200 /// * `since`: The cursor to start from (exclusive), or `None` to start from
201 /// the beginning
202 /// * `until`: The ULID threshold representing 7 days ago
203 /// * `limit`: The maximum number of grants to delete in this batch
204 ///
205 /// # Errors
206 ///
207 /// Returns [`Self::Error`] if the underlying repository fails
208 async fn cleanup(
209 &mut self,
210 since: Option<Ulid>,
211 until: Ulid,
212 limit: usize,
213 ) -> Result<(usize, Option<Ulid>), Self::Error>;
214}
215
216repository_impl!(OAuth2DeviceCodeGrantRepository:
217 async fn add(
218 &mut self,
219 rng: &mut (dyn RngCore + Send),
220 clock: &dyn Clock,
221 params: OAuth2DeviceCodeGrantParams<'_>,
222 ) -> Result<DeviceCodeGrant, Self::Error>;
223
224 async fn lookup(&mut self, id: Ulid) -> Result<Option<DeviceCodeGrant>, Self::Error>;
225
226 async fn find_by_device_code(
227 &mut self,
228 device_code: &str,
229 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
230
231 async fn find_by_user_code(
232 &mut self,
233 user_code: &str,
234 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
235
236 async fn fulfill(
237 &mut self,
238 clock: &dyn Clock,
239 device_code_grant: DeviceCodeGrant,
240 browser_session: &BrowserSession,
241 locale: Option<String>,
242 ) -> Result<DeviceCodeGrant, Self::Error>;
243
244 async fn reject(
245 &mut self,
246 clock: &dyn Clock,
247 device_code_grant: DeviceCodeGrant,
248 browser_session: &BrowserSession,
249 ) -> Result<DeviceCodeGrant, Self::Error>;
250
251 async fn exchange(
252 &mut self,
253 clock: &dyn Clock,
254 device_code_grant: DeviceCodeGrant,
255 session: &Session,
256 ) -> Result<DeviceCodeGrant, Self::Error>;
257
258 async fn cleanup(
259 &mut self,
260 since: Option<Ulid>,
261 until: Ulid,
262 limit: usize,
263 ) -> Result<(usize, Option<Ulid>), Self::Error>;
264);