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