Skip to main content

alibi_plugins/oauth_provider/
token.rs

1//! OAuth authorization-code token exchange and single-use grant lifecycle.
2use 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    /// Exchange an OAuth grant through the provider's public server operation.
12    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        // Match authorization's policy using the consumed grant's scopes and
165        // nonce, but only after binding and client authentication have passed.
166        // Required clients reject an absent verifier before challenge matching.
167        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, &params, "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}