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 ///
127 /// # Errors
128 ///
129 /// Returns [`Self::Error`] if the underlying repository fails or if the
130 /// device code grant is not in the [`Pending`] state
131 ///
132 /// [`Pending`]: mas_data_model::DeviceCodeGrantState::Pending
133 async fn fulfill(
134 &mut self,
135 clock: &dyn Clock,
136 device_code_grant: DeviceCodeGrant,
137 browser_session: &BrowserSession,
138 ) -> Result<DeviceCodeGrant, Self::Error>;
139
140 /// Mark the device code grant as rejected with the given browser session
141 ///
142 /// Returns the updated device code grant
143 ///
144 /// # Parameters
145 ///
146 /// * `clock`: The clock used to generate timestamps
147 /// * `device_code_grant`: The device code grant to reject
148 /// * `browser_session`: The browser session which was used to reject the
149 /// device code grant
150 ///
151 /// # Errors
152 ///
153 /// Returns [`Self::Error`] if the underlying repository fails or if the
154 /// device code grant is not in the [`Pending`] state
155 ///
156 /// [`Pending`]: mas_data_model::DeviceCodeGrantState::Pending
157 async fn reject(
158 &mut self,
159 clock: &dyn Clock,
160 device_code_grant: DeviceCodeGrant,
161 browser_session: &BrowserSession,
162 ) -> Result<DeviceCodeGrant, Self::Error>;
163
164 /// Mark the device code grant as exchanged and store the session which was
165 /// created
166 ///
167 /// Returns the updated device code grant
168 ///
169 /// # Parameters
170 ///
171 /// * `clock`: The clock used to generate timestamps
172 /// * `device_code_grant`: The device code grant to exchange
173 /// * `session`: The OAuth 2.0 session which was created
174 ///
175 /// # Errors
176 ///
177 /// Returns [`Self::Error`] if the underlying repository fails or if the
178 /// device code grant is not in the [`Fulfilled`] state
179 ///
180 /// [`Fulfilled`]: mas_data_model::DeviceCodeGrantState::Fulfilled
181 async fn exchange(
182 &mut self,
183 clock: &dyn Clock,
184 device_code_grant: DeviceCodeGrant,
185 session: &Session,
186 ) -> Result<DeviceCodeGrant, Self::Error>;
187
188 /// Cleanup old device code grants
189 ///
190 /// This will delete device code grants that were created before `until`.
191 /// Uses ULID cursor-based pagination for efficiency.
192 ///
193 /// Returns the number of grants deleted and the cursor for the next batch
194 ///
195 /// # Parameters
196 ///
197 /// * `since`: The cursor to start from (exclusive), or `None` to start from
198 /// the beginning
199 /// * `until`: The ULID threshold representing 7 days ago
200 /// * `limit`: The maximum number of grants to delete in this batch
201 ///
202 /// # Errors
203 ///
204 /// Returns [`Self::Error`] if the underlying repository fails
205 async fn cleanup(
206 &mut self,
207 since: Option<Ulid>,
208 until: Ulid,
209 limit: usize,
210 ) -> Result<(usize, Option<Ulid>), Self::Error>;
211}
212
213repository_impl!(OAuth2DeviceCodeGrantRepository:
214 async fn add(
215 &mut self,
216 rng: &mut (dyn RngCore + Send),
217 clock: &dyn Clock,
218 params: OAuth2DeviceCodeGrantParams<'_>,
219 ) -> Result<DeviceCodeGrant, Self::Error>;
220
221 async fn lookup(&mut self, id: Ulid) -> Result<Option<DeviceCodeGrant>, Self::Error>;
222
223 async fn find_by_device_code(
224 &mut self,
225 device_code: &str,
226 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
227
228 async fn find_by_user_code(
229 &mut self,
230 user_code: &str,
231 ) -> Result<Option<DeviceCodeGrant>, Self::Error>;
232
233 async fn fulfill(
234 &mut self,
235 clock: &dyn Clock,
236 device_code_grant: DeviceCodeGrant,
237 browser_session: &BrowserSession,
238 ) -> Result<DeviceCodeGrant, Self::Error>;
239
240 async fn reject(
241 &mut self,
242 clock: &dyn Clock,
243 device_code_grant: DeviceCodeGrant,
244 browser_session: &BrowserSession,
245 ) -> Result<DeviceCodeGrant, Self::Error>;
246
247 async fn exchange(
248 &mut self,
249 clock: &dyn Clock,
250 device_code_grant: DeviceCodeGrant,
251 session: &Session,
252 ) -> Result<DeviceCodeGrant, Self::Error>;
253
254 async fn cleanup(
255 &mut self,
256 since: Option<Ulid>,
257 until: Ulid,
258 limit: usize,
259 ) -> Result<(usize, Option<Ulid>), Self::Error>;
260);