alibi_plugins/oauth_provider/
userinfo.rs1use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{EndpointResponse, ServerEndpoint};
4use alibi_core::session::SessionRequest;
5use alibi_core::store::{OAuthToken, OAuthTokenKind};
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthResult, AuthSchema, AuthSession, HttpMethod};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde_json::{Value, json};
10use sha2::{Digest as _, Sha256};
11pub(super) async fn live_access<S: AuthSchema>(
13 token: &str,
14 ctx: &AuthContext<S>,
15) -> AuthResult<Option<OAuthToken>> {
16 let hash = URL_SAFE_NO_PAD.encode(Sha256::digest(token));
17 let Some(row) = ctx
18 .database
19 .get_oauth_token(OAuthTokenKind::Access, &hash)
20 .await?
21 else {
22 return Ok(None);
23 };
24 if row.expires_at < chrono::Utc::now()
25 || row.metadata.get("revoked").is_some_and(|v| !v.is_null())
26 {
27 return Ok(None);
28 }
29 let Some(client) = ctx.database.get_oauth_client(&row.client_id).await? else {
30 return Ok(None);
31 };
32 if client.metadata.get("disabled") == Some(&Value::Bool(true)) {
33 return Ok(None);
34 }
35 if let Some(session) = row.session_id.as_deref() {
36 let sessions = if let Some(user) = row.user_id.as_deref() {
37 ctx.database.get_user_sessions(user).await?
38 } else {
39 Vec::new()
40 };
41 if !sessions
42 .iter()
43 .any(|value| value.id().as_ref() == session && value.expires_at() >= chrono::Utc::now())
44 {
45 return Ok(None);
46 }
47 }
48 Ok(Some(row))
49}
50impl OAuthProviderPlugin {
51 #[must_use]
53 pub fn oauth2_user_info_endpoint() -> ServerEndpoint<Value> {
54 ServerEndpoint::new("oauth-provider", "oauth2UserInfo")
55 }
56 pub(super) async fn user_info<S: AuthSchema>(
57 &self,
58 input: Option<&JsValue>,
59 req: &impl SessionRequest,
60 ctx: &AuthContext<S>,
61 ) -> AuthResult<EndpointResponse> {
62 let headers = req.session_headers();
63 let header = headers
64 .iter()
65 .find(|(name, _)| name.eq_ignore_ascii_case("authorization"))
66 .map(|(_, value)| value.trim())
67 .filter(|value| !value.is_empty());
68 let authorization = header.map(|value| {
69 let index = value.find(char::is_whitespace);
70 if let Some(index) = index {
71 let (scheme, rest) = value.split_at(index);
72 let rest = rest.trim();
73 if scheme.eq_ignore_ascii_case("Bearer") {
74 return (rest, "Bearer");
75 }
76 if scheme.eq_ignore_ascii_case("DPoP") {
77 return (rest, "DPoP");
78 }
79 }
80 (value, "Unknown")
81 });
82 let body_token = if req
83 .physical_request()
84 .is_some_and(|request| *request.method() == HttpMethod::Post)
85 {
86 input
87 .and_then(|value| value.get("access_token"))
88 .and_then(JsValue::as_str)
89 .filter(|value| !value.is_empty())
90 } else {
91 None
92 };
93 if authorization.is_some() && body_token.is_some() {
94 return error(
95 400,
96 "invalid_request",
97 "Multiple access token transport methods are not allowed",
98 );
99 }
100 let Some((token, scheme)) =
101 authorization.or_else(|| body_token.map(|token| (token, "Bearer")))
102 else {
103 return error(401, "invalid_request", "access token not found");
104 };
105 let Some(row) = live_access(token, ctx).await? else {
106 return invalid_token();
107 };
108 if scheme == "DPoP" {
109 return error(
110 401,
111 "invalid_token",
112 "DPoP authorization requires a DPoP-bound access token",
113 );
114 }
115 let scopes = row
116 .metadata
117 .get("scopes")
118 .and_then(Value::as_array)
119 .map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
120 .unwrap_or_default();
121 if !scopes.contains(&"openid") {
122 return error(400, "invalid_scope", "Missing required scope");
123 }
124 let Some(id) = row.user_id.as_deref() else {
125 return error(400, "invalid_request", "user not found");
126 };
127 let Some(user) = ctx.database.get_user_by_id(id).await? else {
128 return error(400, "invalid_request", "user not found");
129 };
130 let user = ctx.user_view(&user);
131 let mut claims = serde_json::Map::from_iter([("sub".into(), json!(user.id))]);
132 if scopes.contains(&"profile") {
133 if let Some(name) = user.name {
134 _ = claims.insert("name".into(), json!(name));
135 let parts = name
136 .split(' ')
137 .filter(|value| !value.is_empty())
138 .collect::<Vec<_>>();
139 if let Some((family, given)) = parts.split_last()
140 && !given.is_empty()
141 {
142 _ = claims.insert("given_name".into(), json!(given.join(" ")));
143 _ = claims.insert("family_name".into(), json!(family));
144 }
145 }
146 if let Some(image) = user.image {
147 _ = claims.insert("picture".into(), json!(image));
148 }
149 }
150 if scopes.contains(&"email") {
151 if let Some(email) = user.email {
152 _ = claims.insert("email".into(), json!(email));
153 }
154 _ = claims.insert("email_verified".into(), json!(user.email_verified));
155 }
156 Ok(no_store(EndpointResponse::json(&claims)?))
157 }
158}
159fn no_store(response: EndpointResponse) -> EndpointResponse {
160 response
161 .with_header("Cache-Control", "no-store")
162 .with_header("Pragma", "no-cache")
163}
164fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
165 super::clients::status_protocol_error(status, code, description).map(no_store)
166}
167fn invalid_token() -> AuthResult<EndpointResponse> {
168 error(401, "invalid_token", "Invalid access token").map(|response| {
169 response.with_header(
170 "WWW-Authenticate",
171 "Bearer error=\"invalid_token\", error_description=\"Invalid access token\"",
172 )
173 })
174}