mas_handlers/graphql/model/
oauth.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use anyhow::Context as _;
8use async_graphql::{Context, Description, Enum, ID, Object};
9use chrono::{DateTime, Utc};
10use mas_storage::{oauth2::OAuth2ClientRepository, user::BrowserSessionRepository};
11use oauth2_types::{oidc::ApplicationType, scope::Scope};
12use ulid::Ulid;
13use url::Url;
14
15use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
16use crate::graphql::{UserId, state::ContextExt};
17
18/// An OAuth 2.0 session represents a client session which used the OAuth APIs
19/// to login.
20#[derive(Description)]
21pub struct OAuth2Session(pub mas_data_model::Session);
22
23#[Object(use_type_description)]
24impl OAuth2Session {
25    /// ID of the object.
26    pub async fn id(&self) -> ID {
27        NodeType::OAuth2Session.id(self.0.id)
28    }
29
30    /// OAuth 2.0 client used by this session.
31    pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
32        let state = ctx.state();
33        let mut repo = state.repository().await?;
34        let client = repo
35            .oauth2_client()
36            .lookup(self.0.client_id)
37            .await?
38            .context("Could not load client")?;
39        repo.cancel().await?;
40
41        Ok(OAuth2Client(client))
42    }
43
44    /// Scope granted for this session.
45    pub async fn scope(&self) -> String {
46        self.0.scope.to_string()
47    }
48
49    /// When the object was created.
50    pub async fn created_at(&self) -> DateTime<Utc> {
51        self.0.created_at
52    }
53
54    /// When the session ended.
55    pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
56        match &self.0.state {
57            mas_data_model::SessionState::Valid => None,
58            mas_data_model::SessionState::Finished { finished_at } => Some(*finished_at),
59        }
60    }
61
62    /// The user-agent with which the session was created.
63    pub async fn user_agent(&self) -> Option<UserAgent> {
64        self.0
65            .user_agent
66            .clone()
67            .map(mas_data_model::UserAgent::parse)
68            .map(UserAgent::from)
69    }
70
71    /// The state of the session.
72    pub async fn state(&self) -> SessionState {
73        match &self.0.state {
74            mas_data_model::SessionState::Valid => SessionState::Active,
75            mas_data_model::SessionState::Finished { .. } => SessionState::Finished,
76        }
77    }
78
79    /// The browser session which started this OAuth 2.0 session.
80    pub async fn browser_session(
81        &self,
82        ctx: &Context<'_>,
83    ) -> Result<Option<BrowserSession>, async_graphql::Error> {
84        let Some(user_session_id) = self.0.user_session_id else {
85            return Ok(None);
86        };
87
88        let state = ctx.state();
89        let mut repo = state.repository().await?;
90        let browser_session = repo
91            .browser_session()
92            .lookup(user_session_id)
93            .await?
94            .context("Could not load browser session")?;
95        repo.cancel().await?;
96
97        Ok(Some(BrowserSession(browser_session)))
98    }
99
100    /// User authorized for this session.
101    pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
102        let state = ctx.state();
103        let Some(user_id) = self.0.user_id else {
104            return Ok(None);
105        };
106
107        if !ctx.requester().is_owner_or_admin(&UserId(user_id)) {
108            return Err(async_graphql::Error::new("Unauthorized"));
109        }
110
111        let mut repo = state.repository().await?;
112        let user = repo
113            .user()
114            .lookup(user_id)
115            .await?
116            .context("Could not load user")?;
117        repo.cancel().await?;
118
119        Ok(Some(User(user)))
120    }
121
122    /// The last IP address used by the session.
123    pub async fn last_active_ip(&self) -> Option<String> {
124        self.0.last_active_ip.map(|ip| ip.to_string())
125    }
126
127    /// The last time the session was active.
128    pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
129        self.0.last_active_at
130    }
131}
132
133/// The application type advertised by the client.
134#[derive(Enum, Copy, Clone, Eq, PartialEq)]
135pub enum OAuth2ApplicationType {
136    /// Client is a web application.
137    Web,
138
139    /// Client is a native application.
140    Native,
141}
142
143/// An OAuth 2.0 client
144#[derive(Description)]
145pub struct OAuth2Client(pub mas_data_model::Client);
146
147#[Object(use_type_description)]
148impl OAuth2Client {
149    /// ID of the object.
150    pub async fn id(&self) -> ID {
151        NodeType::OAuth2Client.id(self.0.id)
152    }
153
154    /// OAuth 2.0 client ID
155    pub async fn client_id(&self) -> &str {
156        &self.0.client_id
157    }
158
159    /// Client name advertised by the client.
160    pub async fn client_name(&self) -> Option<&str> {
161        self.0.client_name.as_deref()
162    }
163
164    /// Client URI advertised by the client.
165    pub async fn client_uri(&self) -> Option<&Url> {
166        self.0.client_uri.as_ref()
167    }
168
169    /// Logo URI advertised by the client.
170    pub async fn logo_uri(&self) -> Option<&Url> {
171        self.0.logo_uri.as_ref()
172    }
173
174    /// Terms of services URI advertised by the client.
175    pub async fn tos_uri(&self) -> Option<&Url> {
176        self.0.tos_uri.as_ref()
177    }
178
179    /// Privacy policy URI advertised by the client.
180    pub async fn policy_uri(&self) -> Option<&Url> {
181        self.0.policy_uri.as_ref()
182    }
183
184    /// List of redirect URIs used for authorization grants by the client.
185    pub async fn redirect_uris(&self) -> &[Url] {
186        &self.0.redirect_uris
187    }
188
189    /// The application type advertised by the client.
190    pub async fn application_type(&self) -> Option<OAuth2ApplicationType> {
191        match self.0.application_type.as_ref()? {
192            ApplicationType::Web => Some(OAuth2ApplicationType::Web),
193            ApplicationType::Native => Some(OAuth2ApplicationType::Native),
194            ApplicationType::Unknown(_) => None,
195        }
196    }
197}
198
199/// An OAuth 2.0 consent represents the scope a user consented to grant to a
200/// client.
201#[derive(Description)]
202pub struct OAuth2Consent {
203    scope: Scope,
204    client_id: Ulid,
205}
206
207#[Object(use_type_description)]
208impl OAuth2Consent {
209    /// Scope consented by the user for this client.
210    pub async fn scope(&self) -> String {
211        self.scope.to_string()
212    }
213
214    /// OAuth 2.0 client for which the user granted access.
215    pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
216        let state = ctx.state();
217        let mut repo = state.repository().await?;
218        let client = repo
219            .oauth2_client()
220            .lookup(self.client_id)
221            .await?
222            .context("Could not load client")?;
223        repo.cancel().await?;
224
225        Ok(OAuth2Client(client))
226    }
227}