mas_handlers/oauth2/
userinfo.rs1use 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 if !session.scope.contains("openid") {
114 return Err(RouteError::Unauthorized);
115 }
116
117 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}