Skip to main content

mas_data_model/oauth2/
authorization_grant.rs

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