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);