alibi_plugins/oauth_provider/
userinfo.rs1use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{EndpointResponse, ServerEndpoint};
4use alibi_core::session::SessionRequest;
5use alibi_core::store::{OAuthClient, OAuthToken};
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, AuthSession, HttpMethod};
8use serde_json::{Value, json};
9pub(super) async fn live_access_row<S: AuthSchema>(
11 row: OAuthToken,
12 ctx: &AuthContext<S>,
13 provider: &OAuthProviderPlugin,
14) -> AuthResult<Option<(OAuthToken, OAuthClient)>> {
15 if row.expires_at < chrono::Utc::now()
16 || row.metadata.get("revoked").is_some_and(|v| !v.is_null())
17 {
18 return Ok(None);
19 }
20 let Some(client) = provider.get_client(ctx, &row.client_id).await? else {
21 return Ok(None);
22 };
23 if client.metadata.get("disabled") == Some(&Value::Bool(true)) {
24 return Ok(None);
25 }
26 if let Some(session) = row.session_id.as_deref() {
27 let sessions = if ctx.config.session.secondary_storage.is_some()
28 && ctx.config.session.store_in_database
29 && ctx.config.session.preserve_in_database
30 && !ctx.config.session.stateless
31 {
32 ctx.database
36 .get_session_by_id(session)
37 .await?
38 .into_iter()
39 .collect()
40 } else if let Some(user) = row.user_id.as_deref() {
41 ctx.database.get_user_sessions(user).await?
42 } else {
43 Vec::new()
44 };
45 if !sessions
46 .iter()
47 .any(|value| value.id().as_ref() == session && value.expires_at() >= chrono::Utc::now())
48 {
49 return Ok(None);
50 }
51 }
52 Ok(Some((row, client)))
53}
54impl OAuthProviderPlugin {
55 #[must_use]
57 pub fn oauth2_user_info_endpoint() -> ServerEndpoint<Value> {
58 ServerEndpoint::new("oauth-provider", "oauth2UserInfo")
59 }
60 pub(super) async fn user_info<S: AuthSchema>(
61 &self,
62 input: Option<&JsValue>,
63 req: &impl SessionRequest,
64 ctx: &AuthContext<S>,
65 ) -> AuthResult<EndpointResponse> {
66 let headers = req.session_headers();
67 let header = headers
68 .iter()
69 .find(|(name, _)| name.eq_ignore_ascii_case("authorization"))
70 .map(|(_, value)| value.as_str());
71 let authorization = crate::oauth2::parse_access_token_authorization(header);
72 let body_token = if req
73 .physical_request()
74 .is_some_and(|request| *request.method() == HttpMethod::Post)
75 {
76 input
77 .and_then(|value| value.get("access_token"))
78 .and_then(JsValue::as_str)
79 .filter(|value| !value.is_empty())
80 } else {
81 None
82 };
83 if authorization.is_some() && body_token.is_some() {
84 return error(
85 400,
86 "invalid_request",
87 "Multiple access token transport methods are not allowed",
88 );
89 }
90 let Some(authorization) = authorization.or_else(|| {
91 body_token.map(|token| crate::oauth2::AccessTokenAuthorization {
92 scheme: "Bearer".into(),
93 token: token.into(),
94 })
95 }) else {
96 return error(401, "invalid_request", "access token not found");
97 };
98 let validation = match self
99 .require_active_access_token_with_claims(ctx, &authorization.token, None)
100 .await
101 {
102 Ok(payload) => payload,
103 Err(error) => {
104 let mut response = EndpointResponse::error(error.error);
105 if let Some(body) = error.body {
106 response = response.with_error_body(*body);
107 }
108 if let Some(headers) = error.headers {
109 response.merge_headers(headers);
110 }
111 return Ok(no_store(response));
112 }
113 };
114 let payload = validation.payload;
115 let requested = validation.requested_user_info_claims;
116 if payload
117 .get("cnf")
118 .and_then(crate::oauth2::get_confirmation_jkt)
119 .is_some()
120 && req.physical_request().is_none()
121 {
122 return error(
123 401,
124 "invalid_token",
125 "DPoP-bound access token requires an HTTP request context",
126 );
127 }
128 let method = match req.physical_request().map(alibi_core::AuthRequest::method) {
129 Some(HttpMethod::Post) => "POST",
130 Some(HttpMethod::Put) => "PUT",
131 Some(HttpMethod::Patch) => "PATCH",
132 Some(HttpMethod::Delete) => "DELETE",
133 Some(HttpMethod::Options) => "OPTIONS",
134 Some(HttpMethod::Head) => "HEAD",
135 Some(HttpMethod::Get) | None => "GET",
136 };
137 let url = super::dpop::endpoint_url(ctx, req, "/oauth2/userinfo");
138 let replay = crate::oauth2::create_dpop_replay_store(ctx);
139 let mut binding =
140 crate::oauth2::EnforceDpopBindingParams::new(&payload, &authorization, method, &url);
141 binding.proof_jwt = super::dpop::proof_header(req);
142 binding.replay_store = Some(&replay);
143 binding.proof_max_age_seconds = self.config.dpop_proof_max_age_seconds;
144 binding.signing_algorithms = self.config.dpop_signing_algorithms.clone();
145 if let Err(failure) = crate::oauth2::enforce_dpop_binding(&binding).await {
146 if crate::oauth2::is_dpop_binding_error(&failure) {
147 return error(
148 401,
149 failure.code.as_deref().unwrap_or_default(),
150 &failure.message,
151 );
152 }
153 return Err(AuthError::internal(failure.message));
154 }
155 let scopes = payload
156 .get("scope")
157 .and_then(Value::as_str)
158 .map(|scope| scope.split(' ').collect::<Vec<_>>())
159 .unwrap_or_default();
160 if !scopes.contains(&"openid") {
161 return error(400, "invalid_scope", "Missing required scope");
162 }
163 let Some(id) = payload
164 .get("sub")
165 .and_then(Value::as_str)
166 .filter(|value| !value.is_empty())
167 else {
168 return error(400, "invalid_request", "user not found");
169 };
170 let Some(user) = ctx.database.get_user_by_id(id).await? else {
171 return error(400, "invalid_request", "user not found");
172 };
173 let user = ctx.user_view(&user);
174 let client_id = payload
175 .get("client_id")
176 .or_else(|| payload.get("azp"))
177 .and_then(Value::as_str);
178 let client = if self
179 .config
180 .pairwise_secret
181 .as_ref()
182 .is_some_and(|secret| !secret.is_empty())
183 || self
184 .config
185 .extensions
186 .iter()
187 .any(|extension| extension.claims.user_info.is_some())
188 {
189 if let Some(client_id) = client_id {
190 self.get_client(ctx, client_id).await?
191 } else {
192 None
193 }
194 } else {
195 None
196 };
197 let subject = client.as_ref().map_or_else(
198 || Ok(user.id.clone()),
199 |client| self.resolve_subject(&user.id, client),
200 )?;
201 let mut claims = serde_json::Map::from_iter([("sub".into(), json!(subject))]);
202 let wanted = |name: &str, scope: &str| {
203 scopes.contains(&scope) || requested.iter().any(|claim| claim == name)
204 };
205 if let Some(name) = user.name.as_deref() {
206 if wanted("name", "profile") {
207 _ = claims.insert("name".into(), json!(name));
208 }
209 if wanted("given_name", "profile") || wanted("family_name", "profile") {
210 let parts = name
211 .split(' ')
212 .filter(|part| !part.is_empty())
213 .collect::<Vec<_>>();
214 if let Some((family, given)) = parts.split_last()
215 && !given.is_empty()
216 {
217 if wanted("given_name", "profile") {
218 _ = claims.insert("given_name".into(), json!(given.join(" ")));
219 }
220 if wanted("family_name", "profile") {
221 _ = claims.insert("family_name".into(), json!(family));
222 }
223 }
224 }
225 }
226 if wanted("picture", "profile")
227 && let Some(image) = user.image.as_ref()
228 {
229 _ = claims.insert("picture".into(), json!(image));
230 }
231 if wanted("email", "email")
232 && let Some(email) = user.email.as_ref()
233 {
234 _ = claims.insert("email".into(), json!(email));
235 }
236 if wanted("email_verified", "email") {
237 _ = claims.insert("email_verified".into(), json!(user.email_verified));
238 }
239 let scopes = scopes.into_iter().map(str::to_owned).collect::<Vec<_>>();
240 let input = super::OAuthUserInfoClaimsInput {
241 context: super::extension_claims::callback_context(ctx, req),
242 options: &self.config,
243 user: &user,
244 client: client.as_ref(),
245 scopes: &scopes,
246 payload: &payload,
247 requested_claims: &requested,
248 };
249 let extensions = self.collect_user_info_claims(input.clone()).await?;
250 super::extension_claims::merge_additive(&mut claims, extensions);
251 if let Some(contributor) = &self.config.custom_user_info_claims {
252 let operator = contributor.contribute(input).await?;
256 claims.extend(operator.into_iter().filter(|(key, _)| key != "__proto__"));
257 }
258 _ = claims.insert("sub".into(), json!(subject));
260 Ok(no_store(EndpointResponse::json(&claims)?))
261 }
262}
263fn no_store(response: EndpointResponse) -> EndpointResponse {
264 response
265 .with_header("Cache-Control", "no-store")
266 .with_header("Pragma", "no-cache")
267}
268fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
269 super::clients::status_protocol_error(status, code, description).map(no_store)
270}