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::InvalidTransitionError;
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        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        session: &Session,
88    ) -> Result<Self, InvalidTransitionError> {
89        match self {
90            Self::Pending => Ok(Self::Fulfilled {
91                fulfilled_at,
92                session_id: session.id,
93            }),
94            _ => Err(InvalidTransitionError),
95        }
96    }
97
98    fn exchange(self, exchanged_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
99        match self {
100            Self::Fulfilled {
101                fulfilled_at,
102                session_id,
103            } => Ok(Self::Exchanged {
104                fulfilled_at,
105                exchanged_at,
106                session_id,
107            }),
108            _ => Err(InvalidTransitionError),
109        }
110    }
111
112    fn cancel(self, cancelled_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
113        match self {
114            Self::Pending => Ok(Self::Cancelled { cancelled_at }),
115            _ => Err(InvalidTransitionError),
116        }
117    }
118
119    /// Returns `true` if the authorization grant stage is [`Pending`].
120    ///
121    /// [`Pending`]: AuthorizationGrantStage::Pending
122    #[must_use]
123    pub fn is_pending(&self) -> bool {
124        matches!(self, Self::Pending)
125    }
126
127    /// Returns `true` if the authorization grant stage is [`Fulfilled`].
128    ///
129    /// [`Fulfilled`]: AuthorizationGrantStage::Fulfilled
130    #[must_use]
131    pub fn is_fulfilled(&self) -> bool {
132        matches!(self, Self::Fulfilled { .. })
133    }
134
135    /// Returns `true` if the authorization grant stage is [`Exchanged`].
136    ///
137    /// [`Exchanged`]: AuthorizationGrantStage::Exchanged
138    #[must_use]
139    pub fn is_exchanged(&self) -> bool {
140        matches!(self, Self::Exchanged { .. })
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
145pub struct AuthorizationGrant {
146    pub id: Ulid,
147    #[serde(flatten)]
148    pub stage: AuthorizationGrantStage,
149    pub code: Option<AuthorizationCode>,
150    pub client_id: Ulid,
151    pub redirect_uri: Url,
152    pub scope: Scope,
153    pub state: Option<String>,
154    pub nonce: Option<String>,
155    pub response_mode: ResponseMode,
156    pub response_type_id_token: bool,
157    pub created_at: DateTime<Utc>,
158    pub login_hint: Option<String>,
159    pub locale: Option<String>,
160    /// Raw query parameters from the downstream authorization request, used
161    /// to template the parameters forwarded to the upstream provider.
162    pub raw_parameters: BTreeMap<String, String>,
163}
164
165impl std::ops::Deref for AuthorizationGrant {
166    type Target = AuthorizationGrantStage;
167
168    fn deref(&self) -> &Self::Target {
169        &self.stage
170    }
171}
172
173impl AuthorizationGrant {
174    /// Mark the authorization grant as exchanged.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the authorization grant is not [`Fulfilled`].
179    ///
180    /// [`Fulfilled`]: AuthorizationGrantStage::Fulfilled
181    pub fn exchange(mut self, exchanged_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
182        self.stage = self.stage.exchange(exchanged_at)?;
183        Ok(self)
184    }
185
186    /// Mark the authorization grant as fulfilled.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if the authorization grant is not [`Pending`].
191    ///
192    /// [`Pending`]: AuthorizationGrantStage::Pending
193    pub fn fulfill(
194        mut self,
195        fulfilled_at: DateTime<Utc>,
196        session: &Session,
197    ) -> Result<Self, InvalidTransitionError> {
198        self.stage = self.stage.fulfill(fulfilled_at, session)?;
199        Ok(self)
200    }
201
202    /// Mark the authorization grant as cancelled.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the authorization grant is not [`Pending`].
207    ///
208    /// [`Pending`]: AuthorizationGrantStage::Pending
209    ///
210    /// # TODO
211    ///
212    /// This appears to be unused
213    pub fn cancel(mut self, canceld_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
214        self.stage = self.stage.cancel(canceld_at)?;
215        Ok(self)
216    }
217
218    #[doc(hidden)]
219    pub fn sample(now: DateTime<Utc>, rng: &mut impl RngCore) -> Self {
220        Self {
221            id: Ulid::from_datetime_with_source(now.into(), rng),
222            stage: AuthorizationGrantStage::Pending,
223            code: Some(AuthorizationCode {
224                code: Alphanumeric.sample_string(rng, 10),
225                pkce: None,
226            }),
227            client_id: Ulid::from_datetime_with_source(now.into(), rng),
228            redirect_uri: Url::parse("http://localhost:8080").unwrap(),
229            scope: Scope::from_iter([OPENID, PROFILE]),
230            state: Some(Alphanumeric.sample_string(rng, 10)),
231            nonce: Some(Alphanumeric.sample_string(rng, 10)),
232            response_mode: ResponseMode::Query,
233            response_type_id_token: false,
234            created_at: now,
235            login_hint: Some(String::from("mxid:@example-user:example.com")),
236            locale: Some(String::from("fr")),
237            raw_parameters: BTreeMap::new(),
238        }
239    }
240}