Skip to main content

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