Skip to main content

mas_data_model/oauth2/
authorization_grant.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-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::collections::BTreeMap;
9
10use chrono::{DateTime, Utc};
11use mas_iana::oauth::PkceCodeChallengeMethod;
12use oauth2_types::{
13    pkce::{CodeChallengeError, CodeChallengeMethodExt},
14    requests::ResponseMode,
15    scope::{OPENID, PROFILE, Scope},
16};
17use rand::{
18    Rng,
19    distr::{Alphanumeric, SampleString},
20};
21use serde::Serialize;
22use ulid::Ulid;
23use url::Url;
24
25use super::session::Session;
26use crate::{BrowserSession, InvalidTransitionError, UlidExt as _};
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct Pkce {
30    pub challenge_method: PkceCodeChallengeMethod,
31    pub challenge: String,
32}
33
34impl Pkce {
35    /// Create a new PKCE challenge, with the given method and challenge.
36    #[must_use]
37    pub fn new(challenge_method: PkceCodeChallengeMethod, challenge: String) -> Self {
38        Pkce {
39            challenge_method,
40            challenge,
41        }
42    }
43
44    /// Verify the PKCE challenge.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the verifier is invalid.
49    pub fn verify(&self, verifier: &str) -> Result<(), CodeChallengeError> {
50        self.challenge_method.verify(&self.challenge, verifier)
51    }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct AuthorizationCode {
56    pub code: String,
57    pub pkce: Option<Pkce>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
61#[serde(tag = "stage", rename_all = "lowercase")]
62pub enum AuthorizationGrantStage {
63    #[default]
64    Pending,
65    Fulfilled {
66        browser_session_id: Ulid,
67        fulfilled_at: DateTime<Utc>,
68    },
69    Exchanged {
70        session_id: Ulid,
71        fulfilled_at: DateTime<Utc>,
72        exchanged_at: DateTime<Utc>,
73    },
74    Cancelled {
75        cancelled_at: DateTime<Utc>,
76    },
77}
78
79impl AuthorizationGrantStage {
80    #[must_use]
81    pub fn new() -> Self {
82        Self::Pending
83    }
84
85    fn fulfill(
86        self,
87        fulfilled_at: DateTime<Utc>,
88        browser_session: &BrowserSession,
89    ) -> Result<Self, InvalidTransitionError> {
90        match self {
91            Self::Pending => Ok(Self::Fulfilled {
92                fulfilled_at,
93                browser_session_id: browser_session.id,
94            }),
95            _ => Err(InvalidTransitionError),
96        }
97    }
98
99    fn exchange(
100        self,
101        exchanged_at: DateTime<Utc>,
102        session: &Session,
103    ) -> Result<Self, InvalidTransitionError> {
104        match self {
105            Self::Fulfilled {
106                fulfilled_at,
107                browser_session_id: _,
108            } => Ok(Self::Exchanged {
109                fulfilled_at,
110                exchanged_at,
111                session_id: session.id,
112            }),
113            _ => Err(InvalidTransitionError),
114        }
115    }
116
117    fn cancel(self, cancelled_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
118        match self {
119            Self::Pending => Ok(Self::Cancelled { cancelled_at }),
120            _ => Err(InvalidTransitionError),
121        }
122    }
123
124    /// Returns `true` if the authorization grant stage is [`Pending`].
125    ///
126    /// [`Pending`]: AuthorizationGrantStage::Pending
127    #[must_use]
128    pub fn is_pending(&self) -> bool {
129        matches!(self, Self::Pending)
130    }
131
132    /// Returns `true` if the authorization grant stage is [`Fulfilled`].
133    ///
134    /// [`Fulfilled`]: AuthorizationGrantStage::Fulfilled
135    #[must_use]
136    pub fn is_fulfilled(&self) -> bool {
137        matches!(self, Self::Fulfilled { .. })
138    }
139
140    /// Returns `true` if the authorization grant stage is [`Exchanged`].
141    ///
142    /// [`Exchanged`]: AuthorizationGrantStage::Exchanged
143    #[must_use]
144    pub fn is_exchanged(&self) -> bool {
145        matches!(self, Self::Exchanged { .. })
146    }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
150pub struct AuthorizationGrant {
151    pub id: Ulid,
152    #[serde(flatten)]
153    pub stage: AuthorizationGrantStage,
154    pub code: Option<AuthorizationCode>,
155    pub client_id: Ulid,
156    pub redirect_uri: Url,
157    pub scope: Scope,
158    pub state: Option<String>,
159    pub nonce: Option<String>,
160    pub response_mode: ResponseMode,
161    pub response_type_id_token: bool,
162    pub created_at: DateTime<Utc>,
163    pub login_hint: Option<String>,
164    pub locale: Option<String>,
165    /// Raw query parameters from the downstream authorization request, used
166    /// to template the parameters forwarded to the upstream provider.
167    pub raw_parameters: BTreeMap<String, String>,
168}
169
170impl std::ops::Deref for AuthorizationGrant {
171    type Target = AuthorizationGrantStage;
172
173    fn deref(&self) -> &Self::Target {
174        &self.stage
175    }
176}
177
178impl AuthorizationGrant {
179    /// Mark the authorization grant as exchanged.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the authorization grant is not [`Fulfilled`].
184    ///
185    /// [`Fulfilled`]: AuthorizationGrantStage::Fulfilled
186    pub fn exchange(
187        mut self,
188        exchanged_at: DateTime<Utc>,
189        session: &Session,
190    ) -> Result<Self, InvalidTransitionError> {
191        self.stage = self.stage.exchange(exchanged_at, session)?;
192        Ok(self)
193    }
194
195    /// Mark the authorization grant as fulfilled.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the authorization grant is not [`Pending`].
200    ///
201    /// [`Pending`]: AuthorizationGrantStage::Pending
202    pub fn fulfill(
203        mut self,
204        fulfilled_at: DateTime<Utc>,
205        browser_session: &BrowserSession,
206    ) -> Result<Self, InvalidTransitionError> {
207        self.stage = self.stage.fulfill(fulfilled_at, browser_session)?;
208        Ok(self)
209    }
210
211    /// Mark the authorization grant as cancelled.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if the authorization grant is not [`Pending`].
216    ///
217    /// [`Pending`]: AuthorizationGrantStage::Pending
218    ///
219    /// # TODO
220    ///
221    /// This appears to be unused
222    pub fn cancel(mut self, canceld_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
223        self.stage = self.stage.cancel(canceld_at)?;
224        Ok(self)
225    }
226
227    #[doc(hidden)]
228    pub fn sample(now: DateTime<Utc>, rng: &mut impl Rng) -> Self {
229        Self {
230            id: Ulid::from_datetime_with_rng(now, rng),
231            stage: AuthorizationGrantStage::Pending,
232            code: Some(AuthorizationCode {
233                code: Alphanumeric.sample_string(rng, 10),
234                pkce: None,
235            }),
236            client_id: Ulid::from_datetime_with_rng(now, rng),
237            redirect_uri: Url::parse("http://localhost:8080").unwrap(),
238            scope: Scope::from_iter([OPENID, PROFILE]),
239            state: Some(Alphanumeric.sample_string(rng, 10)),
240            nonce: Some(Alphanumeric.sample_string(rng, 10)),
241            response_mode: ResponseMode::Query,
242            response_type_id_token: false,
243            created_at: now,
244            login_hint: Some(String::from("mxid:@example-user:example.com")),
245            locale: Some(String::from("fr")),
246            raw_parameters: BTreeMap::new(),
247        }
248    }
249}