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