mas_handlers/oauth2/
userinfo.rs

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