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