mas_handlers/graphql/model/
oauth.rs

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