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_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 if !session.scope.contains("openid") {
113 return Err(RouteError::Unauthorized);
114 }
115
116 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}