alibi_plugins/oauth_provider/
token.rs1use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{EndpointResponse, ServerEndpoint};
4use alibi_core::session::SessionRequest;
5use alibi_core::store::{CreateOAuthToken, OAuthTokenKind};
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthResult, AuthSchema, AuthSession};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde_json::{Value, json};
10use sha2::{Digest as _, Sha256};
11impl OAuthProviderPlugin {
12 pub fn oauth2_token_endpoint(
14 input: &impl serde::Serialize,
15 ) -> AuthResult<ServerEndpoint<Value>> {
16 ServerEndpoint::new("oauth-provider", "oauth2Token").with_body(input)
17 }
18 pub(super) async fn token<S: AuthSchema>(
19 &self,
20 input: Option<&JsValue>,
21 req: &impl SessionRequest,
22 ctx: &AuthContext<S>,
23 ) -> AuthResult<EndpointResponse> {
24 let body: Value = input
25 .map(JsValue::to_json_value)
26 .transpose()?
27 .unwrap_or_else(|| json!({}));
28 let text = |key: &str| body.get(key).and_then(Value::as_str).unwrap_or_default();
29 let grant = text("grant_type").trim();
30 if !self
31 .config
32 .grant_types
33 .iter()
34 .any(|supported| supported == grant)
35 {
36 return error(
37 400,
38 "unsupported_grant_type",
39 &format!("unsupported grant_type {grant}"),
40 );
41 }
42 if grant == "refresh_token" {
43 return Box::pin(self.refresh_grant(&body, req, ctx)).await;
44 }
45 if grant == "client_credentials" {
46 return Box::pin(self.machine_grant(&body, req, ctx)).await;
47 }
48 if grant != "authorization_code" {
50 return error(
51 400,
52 "invalid_request",
53 if text("client_id").is_empty() {
54 "Missing required client_id"
55 } else if grant == "refresh_token" {
56 "Missing a required refresh_token for refresh_token grant"
57 } else {
58 "Missing required client credentials"
59 },
60 );
61 }
62 let credentials = super::credentials::extract(&body, req);
63 let client_id = credentials.client_id.clone();
64 let secret = &credentials.secret;
65 if client_id.is_empty() {
66 return error(400, "invalid_request", "client_id is required");
67 }
68 let code = text("code");
69 if code.is_empty() {
70 return error(400, "invalid_request", "code is required");
71 }
72 let verifier = text("code_verifier");
73 if secret.is_empty() && verifier.is_empty() {
74 return error(
75 400,
76 "invalid_request",
77 "Either code_verifier or client_secret is required",
78 );
79 }
80 let identifier = URL_SAFE_NO_PAD.encode(Sha256::digest(code));
81 let Some(proof) = ctx.verifications().consume(&identifier).await? else {
82 ctx.database
83 .delete_oauth_tokens_for_code(&identifier)
84 .await?;
85 return error(400, "invalid_grant", "invalid code");
86 };
87 let Ok(value) = serde_json::from_str::<Value>(proof.value()?) else {
88 return error(400, "invalid_grant", "malformed verification value");
89 };
90 if value.get("type").unwrap_or(&Value::Null) != "authorization_code"
91 || !value.get("query").unwrap_or(&Value::Null).is_object()
92 {
93 return error(400, "invalid_grant", "malformed verification value");
94 }
95 let query = &value.get("query").unwrap_or(&Value::Null);
96 if query.get("client_id").and_then(Value::as_str) != Some(client_id.as_str()) {
97 return error(400, "invalid_grant", "invalid client_id");
98 }
99 let bound = query
100 .get("redirect_uri")
101 .unwrap_or(&Value::Null)
102 .as_str()
103 .unwrap_or_default();
104 if !bound.is_empty() && text("redirect_uri").is_empty() {
105 return error(400, "invalid_request", "redirect_uri is required");
106 }
107 if bound != text("redirect_uri") {
108 return error(400, "invalid_grant", "redirect_uri mismatch");
109 }
110 let Some(scope) = query.get("scope").unwrap_or(&Value::Null).as_str() else {
111 return error(500, "invalid_scope", "verification scope unset");
112 };
113 let client = match credentials.authenticate(ctx).await? {
114 Ok(client) => client,
115 Err(response) => {
116 return Ok(response
117 .with_header("Cache-Control", "no-store")
118 .with_header("Pragma", "no-cache"));
119 }
120 };
121 let registered = client
122 .metadata
123 .get("token_endpoint_auth_method")
124 .and_then(Value::as_str)
125 .unwrap_or("client_secret_basic");
126 if client
127 .metadata
128 .get("grant_types")
129 .unwrap_or(&Value::Null)
130 .as_array()
131 .is_some_and(|grants| !grants.iter().any(|g| g == grant))
132 {
133 return error(
134 400,
135 "unauthorized_client",
136 "client is not authorized to use grant type authorization_code",
137 );
138 }
139 let ceiling = client
140 .metadata
141 .get("scope")
142 .unwrap_or(&Value::Null)
143 .as_str()
144 .unwrap_or_default()
145 .split(' ')
146 .collect::<Vec<_>>();
147 let invalid = scope
148 .split(' ')
149 .filter(|s| !ceiling.contains(s))
150 .collect::<Vec<_>>();
151 if !invalid.is_empty() {
152 return error(
153 400,
154 "invalid_scope",
155 &format!("The following scopes are invalid: {}", invalid.join(", ")),
156 );
157 }
158 let challenge = query
159 .get("code_challenge")
160 .unwrap_or(&Value::Null)
161 .as_str()
162 .unwrap_or_default();
163 if registered == "none" && verifier.is_empty() {
164 return error(400, "invalid_request", "PKCE is required for this client");
165 }
166 if !challenge.is_empty() && verifier.is_empty() {
167 return error(
168 401,
169 "invalid_request",
170 "code_verifier required because PKCE was used in authorization",
171 );
172 }
173 if challenge.is_empty() && !verifier.is_empty() {
174 return error(
175 401,
176 "invalid_request",
177 "code_verifier provided but PKCE was not used in authorization",
178 );
179 }
180 if !verifier.is_empty()
181 && (query.get("code_challenge_method").unwrap_or(&Value::Null) != "S256"
182 || URL_SAFE_NO_PAD.encode(Sha256::digest(verifier)) != challenge)
183 {
184 return error(401, "invalid_request", "code verification failed");
185 }
186 let user_id = value
187 .get("userId")
188 .unwrap_or(&Value::Null)
189 .as_str()
190 .unwrap_or_default();
191 if ctx.database.get_user_by_id(user_id).await?.is_none() {
192 return error(
193 400,
194 "invalid_user",
195 "missing user, user may have been deleted",
196 );
197 }
198 let session_id = value
199 .get("sessionId")
200 .unwrap_or(&Value::Null)
201 .as_str()
202 .unwrap_or_default();
203 let session = ctx
204 .database
205 .get_user_sessions(user_id)
206 .await?
207 .into_iter()
208 .find(|s| s.id().as_ref() == session_id);
209 if session.is_none_or(|s| s.expires_at() < chrono::Utc::now()) {
210 return error(400, "invalid_request", "session no longer exists");
211 }
212 let issued = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
213 .ok_or_else(|| alibi_core::AuthError::internal("Invalid token issue time"))?;
214 let expires = issued + chrono::Duration::seconds(3600);
215
216 let random_token = || {
217 rand::RngExt::sample_iter(rand::rng(), rand::distr::Alphanumeric)
218 .map(char::from)
219 .filter(char::is_ascii_alphabetic)
220 .take(32)
221 .collect::<String>()
222 };
223 let scopes = scope.split(' ').collect::<Vec<_>>();
224 let refresh_allowed = client
225 .metadata
226 .get("grant_types")
227 .and_then(Value::as_array)
228 .is_none_or(|grants| grants.iter().any(|g| g == "refresh_token"));
229 let resources = value
230 .get("resource")
231 .filter(|value| value.as_array().is_none_or(|values| !values.is_empty()))
232 .cloned()
233 .unwrap_or(Value::Null);
234 let refresh = if refresh_allowed && scopes.contains(&"offline_access") {
235 let token = random_token();
236 let auth_time = value
237 .get("authTime")
238 .and_then(Value::as_i64)
239 .and_then(chrono::DateTime::from_timestamp_millis);
240 let row = ctx.database.create_oauth_token(OAuthTokenKind::Refresh,CreateOAuthToken {
241 token:URL_SAFE_NO_PAD.encode(Sha256::digest(&token)),client_id:client_id.clone(),user_id:Some(user_id.into()),session_id:Some(session_id.into()),authorization_code_id:Some(identifier.clone()),
242 metadata:json!({"scopes":scopes,"resources":resources,"authTime":auth_time}),created_at:issued,expires_at:issued+chrono::Duration::seconds(2_592_000)
243 }).await?;
244 Some((row.id, token))
245 } else {
246 None
247 };
248 let token = random_token();
249 let mut metadata = json!({"scopes":scopes,"resources":resources});
250 if let Some((id, _)) = &refresh {
251 _ = metadata
252 .as_object_mut()
253 .ok_or_else(|| alibi_core::AuthError::internal("Invalid access-token metadata"))?
254 .insert("refreshId".into(), json!(id));
255 }
256 _ = ctx
257 .database
258 .create_oauth_token(
259 OAuthTokenKind::Access,
260 CreateOAuthToken {
261 token: URL_SAFE_NO_PAD.encode(Sha256::digest(&token)),
262 client_id: client_id.clone(),
263 user_id: Some(user_id.into()),
264 session_id: Some(session_id.into()),
265 authorization_code_id: Some(identifier),
266 metadata,
267 created_at: issued,
268 expires_at: expires,
269 },
270 )
271 .await?;
272 let mut response = json!({"access_token":token,"expires_in":3600,"expires_at":expires.timestamp(),"token_type":"Bearer","scope":scope});
273 if scopes.contains(&"openid") {
274 match Box::pin(self.issue_id_token(&client, &value, &token, req, ctx)).await? {
275 Ok(Some(token)) => {
276 _ = response
277 .as_object_mut()
278 .ok_or_else(|| alibi_core::AuthError::internal("Invalid token response"))?
279 .insert("id_token".into(), json!(token));
280 }
281 Ok(None) => {}
282 Err(response) => {
283 return Ok(response
284 .with_header("Cache-Control", "no-store")
285 .with_header("Pragma", "no-cache"));
286 }
287 }
288 }
289 if let Some((_, token)) = refresh {
290 _ = response
291 .as_object_mut()
292 .ok_or_else(|| alibi_core::AuthError::internal("Invalid token response"))?
293 .insert("refresh_token".into(), json!(token));
294 }
295 Ok(EndpointResponse::json(&response)?
296 .with_header("Cache-Control", "no-store")
297 .with_header("Pragma", "no-cache"))
298 }
299}
300fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
301 super::clients::status_protocol_error(status, code, description).map(|r| {
302 r.with_header("Cache-Control", "no-store")
303 .with_header("Pragma", "no-cache")
304 })
305}