mas_handlers/oauth2/
userinfo.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 axum::{
8    Json,
9    extract::State,
10    response::{IntoResponse, Response},
11};
12use hyper::StatusCode;
13use mas_axum_utils::{
14    jwt::JwtResponse,
15    record_error,
16    user_authorization::{AuthorizationVerificationError, UserAuthorization},
17};
18use mas_data_model::{BoxClock, BoxRng};
19use mas_jose::{
20    constraints::Constrainable,
21    jwt::{JsonWebSignatureHeader, Jwt},
22};
23use mas_keystore::Keystore;
24use mas_router::UrlBuilder;
25use mas_storage::{BoxRepository, oauth2::OAuth2ClientRepository};
26use serde::Serialize;
27use serde_with::skip_serializing_none;
28use thiserror::Error;
29use ulid::Ulid;
30
31use crate::{BoundActivityTracker, impl_from_error_for_route};
32
33#[skip_serializing_none]
34#[derive(Serialize)]
35struct UserInfo {
36    sub: String,
37    username: String,
38}
39
40#[derive(Serialize)]
41struct SignedUserInfo {
42    iss: String,
43    aud: String,
44    #[serde(flatten)]
45    user_info: UserInfo,
46}
47
48#[derive(Debug, Error)]
49pub enum RouteError {
50    #[error(transparent)]
51    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
52
53    #[error("failed to authenticate")]
54    AuthorizationVerificationError(
55        #[from] AuthorizationVerificationError<mas_storage::RepositoryError>,
56    ),
57
58    #[error("session is not allowed to access the userinfo endpoint")]
59    Unauthorized,
60
61    #[error("no suitable key found for signing")]
62    InvalidSigningKey,
63
64    #[error("failed to load client {0}")]
65    NoSuchClient(Ulid),
66
67    #[error("failed to load user {0}")]
68    NoSuchUser(Ulid),
69}
70
71impl_from_error_for_route!(mas_storage::RepositoryError);
72impl_from_error_for_route!(mas_keystore::WrongAlgorithmError);
73impl_from_error_for_route!(mas_jose::jwt::JwtSignatureError);
74
75impl IntoResponse for RouteError {
76    fn into_response(self) -> axum::response::Response {
77        let sentry_event_id = record_error!(
78            self,
79            Self::Internal(_)
80                | Self::InvalidSigningKey
81                | Self::NoSuchClient(_)
82                | Self::NoSuchUser(_)
83        );
84        let response = match self {
85            Self::Internal(_)
86            | Self::InvalidSigningKey
87            | Self::NoSuchClient(_)
88            | Self::NoSuchUser(_) => {
89                (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
90            }
91            Self::AuthorizationVerificationError(_) | Self::Unauthorized => {
92                StatusCode::UNAUTHORIZED.into_response()
93            }
94        };
95
96        (sentry_event_id, response).into_response()
97    }
98}
99
100#[tracing::instrument(name = "handlers.oauth2.userinfo.get", skip_all)]
101pub async fn get(
102    mut rng: BoxRng,
103    clock: BoxClock,
104    State(url_builder): State<UrlBuilder>,
105    activity_tracker: BoundActivityTracker,
106    mut repo: BoxRepository,
107    State(key_store): State<Keystore>,
108    user_authorization: UserAuthorization,
109) -> Result<Response, RouteError> {
110    let session = user_authorization.protected(&mut repo, &clock).await?;
111
112    // This endpoint requires the `openid` scope.
113    if !session.scope.contains("openid") {
114        return Err(RouteError::Unauthorized);
115    }
116
117    // Fail if the session is not associated with a user.
118    let Some(user_id) = session.user_id else {
119        return Err(RouteError::Unauthorized);
120    };
121
122    activity_tracker
123        .record_oauth2_session(&clock, &session)
124        .await;
125
126    let user = repo
127        .user()
128        .lookup(user_id)
129        .await?
130        .ok_or(RouteError::NoSuchUser(user_id))?;
131
132    let user_info = UserInfo {
133        sub: user.sub.clone(),
134        username: user.username.clone(),
135    };
136
137    let client = repo
138        .oauth2_client()
139        .lookup(session.client_id)
140        .await?
141        .ok_or(RouteError::NoSuchClient(session.client_id))?;
142
143    repo.save().await?;
144
145    if let Some(alg) = client.userinfo_signed_response_alg {
146        let key = key_store
147            .signing_key_for_algorithm(&alg)
148            .ok_or(RouteError::InvalidSigningKey)?;
149
150        let signer = key.params().signing_key_for_alg(&alg)?;
151        let header = JsonWebSignatureHeader::new(alg)
152            .with_kid(key.kid().ok_or(RouteError::InvalidSigningKey)?);
153
154        let user_info = SignedUserInfo {
155            iss: url_builder.oidc_issuer().to_string(),
156            aud: client.client_id,
157            user_info,
158        };
159
160        let token = Jwt::sign_with_rng(&mut rng, header, user_info, &signer)?;
161        Ok(JwtResponse(token).into_response())
162    } else {
163        Ok(Json(user_info).into_response())
164    }
165}