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