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 .supported_grant_types()
31 .iter()
32 .any(|supported| supported == grant)
33 {
34 return error(
35 400,
36 "unsupported_grant_type",
37 &format!("unsupported grant_type {grant}"),
38 );
39 }
40 if grant == "refresh_token" {
41 return Box::pin(self.refresh_grant(&body, req, ctx)).await;
42 }
43 if grant == "client_credentials" {
44 return Box::pin(self.machine_grant(&body, req, ctx)).await;
45 }
46 if grant != "authorization_code" {
47 if let Some(response) = self.extension_grant(grant, input, req, ctx).await? {
48 return Ok(response);
49 }
50 return error(
51 400,
52 "unsupported_grant_type",
53 &format!("unsupported grant_type {grant}"),
54 );
55 }
56 let credentials =
57 match Box::pin(super::credentials::extract(&body, req, ctx, self, "token")).await? {
58 Ok(credentials) => credentials,
59 Err(response) => {
60 return Ok(response
61 .with_header("Cache-Control", "no-store")
62 .with_header("Pragma", "no-cache"));
63 }
64 };
65 let client_id = credentials.client_id.clone();
66 let secret = &credentials.secret;
67 if client_id.is_empty() {
68 return error(400, "invalid_request", "client_id is required");
69 }
70 let code = text("code");
71 if code.is_empty() {
72 return error(400, "invalid_request", "code is required");
73 }
74 let verifier = text("code_verifier");
75 if secret.is_empty() && verifier.is_empty() && !credentials.pre_verified {
76 return error(
77 400,
78 "invalid_request",
79 "Either code_verifier or client_secret is required",
80 );
81 }
82 let client = match Box::pin(credentials.authenticate(ctx, self)).await? {
86 Ok(client) => client,
87 Err(response) => {
88 return Ok(response
89 .with_header("Cache-Control", "no-store")
90 .with_header("Pragma", "no-cache"));
91 }
92 };
93 let registered = client
94 .metadata
95 .get("token_endpoint_auth_method")
96 .and_then(Value::as_str)
97 .unwrap_or("client_secret_basic");
98 if client
99 .metadata
100 .get("grant_types")
101 .unwrap_or(&Value::Null)
102 .as_array()
103 .is_some_and(|grants| !grants.iter().any(|g| g == grant))
104 {
105 return error(
106 400,
107 "unauthorized_client",
108 "client is not authorized to use grant type authorization_code",
109 );
110 }
111 let identifier = Box::pin(self.hash_token(code, "authorization_code")).await?;
112 let Some(proof) = Box::pin(ctx.verifications().consume(&identifier)).await? else {
113 Box::pin(ctx.database.delete_oauth_tokens_for_code(&identifier)).await?;
114 return error(400, "invalid_grant", "invalid code");
115 };
116 let Ok(value) = serde_json::from_str::<Value>(proof.value()?) else {
117 return error(400, "invalid_grant", "malformed verification value");
118 };
119 if value.get("type").unwrap_or(&Value::Null) != "authorization_code"
120 || !value.get("query").unwrap_or(&Value::Null).is_object()
121 {
122 return error(400, "invalid_grant", "malformed verification value");
123 }
124 let query = &value.get("query").unwrap_or(&Value::Null);
125 if query.get("client_id").and_then(Value::as_str) != Some(client_id.as_str()) {
126 return error(400, "invalid_grant", "invalid client_id");
127 }
128 let bound = query
129 .get("redirect_uri")
130 .unwrap_or(&Value::Null)
131 .as_str()
132 .unwrap_or_default();
133 if !bound.is_empty() && text("redirect_uri").is_empty() {
134 return error(400, "invalid_request", "redirect_uri is required");
135 }
136 if bound != text("redirect_uri") {
137 return error(400, "invalid_grant", "redirect_uri mismatch");
138 }
139 let Some(scope) = query.get("scope").unwrap_or(&Value::Null).as_str() else {
140 return error(500, "invalid_scope", "verification scope unset");
141 };
142 let ceiling = client
143 .metadata
144 .get("scope")
145 .unwrap_or(&Value::Null)
146 .as_str()
147 .unwrap_or_default()
148 .split(' ')
149 .collect::<Vec<_>>();
150 let invalid = scope
151 .split(' ')
152 .filter(|s| !ceiling.contains(s))
153 .collect::<Vec<_>>();
154 if !invalid.is_empty() {
155 return error(
156 400,
157 "invalid_scope",
158 &format!("The following scopes are invalid: {}", invalid.join(", ")),
159 );
160 }
161 let challenge = query
162 .get("code_challenge")
163 .unwrap_or(&Value::Null)
164 .as_str()
165 .unwrap_or_default();
166 let has_oidc_nonce = scope.split(' ').any(|scope| scope == "openid")
170 && query
171 .get("nonce")
172 .and_then(Value::as_str)
173 .is_some_and(|nonce| !nonce.is_empty());
174 let pkce_required = registered == "none"
175 || client.metadata.get("require_pkce") != Some(&Value::Bool(false))
176 || (scope.split(' ').any(|scope| scope == "offline_access") && !has_oidc_nonce);
177 if pkce_required && verifier.is_empty() {
178 return error(400, "invalid_request", "PKCE is required for this client");
179 }
180 if !challenge.is_empty() && verifier.is_empty() {
181 return error(
182 400,
183 "invalid_request",
184 "code_verifier required because PKCE was used in authorization",
185 );
186 }
187 if challenge.is_empty() && !verifier.is_empty() {
188 return error(
189 400,
190 "invalid_request",
191 "code_verifier provided but PKCE was not used in authorization",
192 );
193 }
194 if !verifier.is_empty()
195 && (query.get("code_challenge_method").unwrap_or(&Value::Null) != "S256"
196 || URL_SAFE_NO_PAD.encode(Sha256::digest(verifier)) != challenge)
197 {
198 return error(400, "invalid_grant", "code verification failed");
199 }
200 let user_id = value
201 .get("userId")
202 .unwrap_or(&Value::Null)
203 .as_str()
204 .unwrap_or_default();
205 let Some(user) = ctx.database.get_user_by_id(user_id).await? else {
206 return error(
207 400,
208 "invalid_user",
209 "missing user, user may have been deleted",
210 );
211 };
212 let session_id = value
213 .get("sessionId")
214 .unwrap_or(&Value::Null)
215 .as_str()
216 .unwrap_or_default();
217 let session = ctx
218 .database
219 .get_user_sessions(user_id)
220 .await?
221 .into_iter()
222 .find(|s| s.id().as_ref() == session_id);
223 if session.is_none_or(|s| s.expires_at() < chrono::Utc::now()) {
224 return error(400, "invalid_request", "session no longer exists");
225 }
226 let mut params = super::OAuthTokenIssueParams::new(
227 client,
228 scope.split(' ').map(str::to_owned).collect(),
229 );
230 params.user = Some(ctx.trusted_user_view(&user));
231 params.verification_value = Some(value.clone());
232 params.reference_id = value
233 .get("referenceId")
234 .and_then(Value::as_str)
235 .map(str::to_owned);
236 let supported = self.supported_claims();
237 params.requested_user_info_claims = Some(
238 query
239 .get("claims")
240 .and_then(super::claims_request::parse)
241 .map(|request| {
242 request
243 .user_info
244 .into_iter()
245 .filter(|name| supported.contains(name))
246 .collect()
247 })
248 .unwrap_or_default(),
249 );
250
251 params.session_id = Some(session_id.into());
252 params.authorization_code_id = Some(identifier);
253 params.auth_time = value
254 .get("authTime")
255 .and_then(Value::as_i64)
256 .and_then(chrono::DateTime::from_timestamp_millis);
257 params.nonce = query
258 .get("nonce")
259 .and_then(Value::as_str)
260 .map(str::to_owned);
261 params.resources = value
262 .get("resource")
263 .filter(|v| v.as_array().is_none_or(|a| !a.is_empty()))
264 .and_then(|v| {
265 v.as_array()
266 .map(|a| {
267 a.iter()
268 .filter_map(Value::as_str)
269 .map(str::to_owned)
270 .collect()
271 })
272 .or_else(|| v.as_str().map(|v| vec![v.into()]))
273 });
274 let response =
275 match Box::pin(self.mint_tokens(ctx, req, ¶ms, "authorization_code")).await {
276 Ok(value) => value,
277 Err(error) => return super::issuance::error_response(error),
278 };
279 Ok(EndpointResponse::json(&response)?
280 .with_header("Cache-Control", "no-store")
281 .with_header("Pragma", "no-cache"))
282 }
283}
284fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
285 super::clients::status_protocol_error(status, code, description).map(|r| {
286 r.with_header("Cache-Control", "no-store")
287 .with_header("Pragma", "no-cache")
288 })
289}