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 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 if !session.scope.contains("openid") {
103 return Err(RouteError::Unauthorized);
104 }
105
106 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}