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 =
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        // RFC 6749 ยง5.2: an unknown or unauthenticated client is
83        // `invalid_client`, so resolve it before the code lookup can report
84        // `invalid_grant` and before a one-time code is consumed.
85        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        // Match authorization's policy using the consumed grant's scopes and
167        // nonce, but only after binding and client authentication have passed.
168        // Required clients reject an absent verifier before challenge matching.
169        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, &params, "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}