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            .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        // Each remaining grant receives its own issuance and lifecycle finding.
48        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, &params).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}