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::store::{CreateOAuthToken, OAuthTokenKind};
6use alibi_core::utils::json::JsValue;
7use alibi_core::{AuthContext, AuthResult, AuthSchema, AuthSession};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde_json::{Value, json};
10use sha2::{Digest as _, Sha256};
11impl OAuthProviderPlugin {
12    /// Exchange an OAuth grant through the provider's public server operation.
13    pub fn oauth2_token_endpoint(
14        input: &impl serde::Serialize,
15    ) -> AuthResult<ServerEndpoint<Value>> {
16        ServerEndpoint::new("oauth-provider", "oauth2Token").with_body(input)
17    }
18    pub(super) async fn token<S: AuthSchema>(
19        &self,
20        input: Option<&JsValue>,
21        req: &impl SessionRequest,
22        ctx: &AuthContext<S>,
23    ) -> AuthResult<EndpointResponse> {
24        let body: Value = input
25            .map(JsValue::to_json_value)
26            .transpose()?
27            .unwrap_or_else(|| json!({}));
28        let text = |key: &str| body.get(key).and_then(Value::as_str).unwrap_or_default();
29        let grant = text("grant_type").trim();
30        if !self
31            .config
32            .grant_types
33            .iter()
34            .any(|supported| supported == grant)
35        {
36            return error(
37                400,
38                "unsupported_grant_type",
39                &format!("unsupported grant_type {grant}"),
40            );
41        }
42        if grant == "refresh_token" {
43            return Box::pin(self.refresh_grant(&body, req, ctx)).await;
44        }
45        if grant == "client_credentials" {
46            return Box::pin(self.machine_grant(&body, req, ctx)).await;
47        }
48        // Each remaining grant receives its own issuance and lifecycle finding.
49        if grant != "authorization_code" {
50            return error(
51                400,
52                "invalid_request",
53                if text("client_id").is_empty() {
54                    "Missing required client_id"
55                } else if grant == "refresh_token" {
56                    "Missing a required refresh_token for refresh_token grant"
57                } else {
58                    "Missing required client credentials"
59                },
60            );
61        }
62        let credentials = super::credentials::extract(&body, req);
63        let client_id = credentials.client_id.clone();
64        let secret = &credentials.secret;
65        if client_id.is_empty() {
66            return error(400, "invalid_request", "client_id is required");
67        }
68        let code = text("code");
69        if code.is_empty() {
70            return error(400, "invalid_request", "code is required");
71        }
72        let verifier = text("code_verifier");
73        if secret.is_empty() && verifier.is_empty() {
74            return error(
75                400,
76                "invalid_request",
77                "Either code_verifier or client_secret is required",
78            );
79        }
80        let identifier = URL_SAFE_NO_PAD.encode(Sha256::digest(code));
81        let Some(proof) = ctx.verifications().consume(&identifier).await? else {
82            ctx.database
83                .delete_oauth_tokens_for_code(&identifier)
84                .await?;
85            return error(400, "invalid_grant", "invalid code");
86        };
87        let Ok(value) = serde_json::from_str::<Value>(proof.value()?) else {
88            return error(400, "invalid_grant", "malformed verification value");
89        };
90        if value.get("type").unwrap_or(&Value::Null) != "authorization_code"
91            || !value.get("query").unwrap_or(&Value::Null).is_object()
92        {
93            return error(400, "invalid_grant", "malformed verification value");
94        }
95        let query = &value.get("query").unwrap_or(&Value::Null);
96        if query.get("client_id").and_then(Value::as_str) != Some(client_id.as_str()) {
97            return error(400, "invalid_grant", "invalid client_id");
98        }
99        let bound = query
100            .get("redirect_uri")
101            .unwrap_or(&Value::Null)
102            .as_str()
103            .unwrap_or_default();
104        if !bound.is_empty() && text("redirect_uri").is_empty() {
105            return error(400, "invalid_request", "redirect_uri is required");
106        }
107        if bound != text("redirect_uri") {
108            return error(400, "invalid_grant", "redirect_uri mismatch");
109        }
110        let Some(scope) = query.get("scope").unwrap_or(&Value::Null).as_str() else {
111            return error(500, "invalid_scope", "verification scope unset");
112        };
113        let client = match credentials.authenticate(ctx).await? {
114            Ok(client) => client,
115            Err(response) => {
116                return Ok(response
117                    .with_header("Cache-Control", "no-store")
118                    .with_header("Pragma", "no-cache"));
119            }
120        };
121        let registered = client
122            .metadata
123            .get("token_endpoint_auth_method")
124            .and_then(Value::as_str)
125            .unwrap_or("client_secret_basic");
126        if client
127            .metadata
128            .get("grant_types")
129            .unwrap_or(&Value::Null)
130            .as_array()
131            .is_some_and(|grants| !grants.iter().any(|g| g == grant))
132        {
133            return error(
134                400,
135                "unauthorized_client",
136                "client is not authorized to use grant type authorization_code",
137            );
138        }
139        let ceiling = client
140            .metadata
141            .get("scope")
142            .unwrap_or(&Value::Null)
143            .as_str()
144            .unwrap_or_default()
145            .split(' ')
146            .collect::<Vec<_>>();
147        let invalid = scope
148            .split(' ')
149            .filter(|s| !ceiling.contains(s))
150            .collect::<Vec<_>>();
151        if !invalid.is_empty() {
152            return error(
153                400,
154                "invalid_scope",
155                &format!("The following scopes are invalid: {}", invalid.join(", ")),
156            );
157        }
158        let challenge = query
159            .get("code_challenge")
160            .unwrap_or(&Value::Null)
161            .as_str()
162            .unwrap_or_default();
163        if registered == "none" && verifier.is_empty() {
164            return error(400, "invalid_request", "PKCE is required for this client");
165        }
166        if !challenge.is_empty() && verifier.is_empty() {
167            return error(
168                401,
169                "invalid_request",
170                "code_verifier required because PKCE was used in authorization",
171            );
172        }
173        if challenge.is_empty() && !verifier.is_empty() {
174            return error(
175                401,
176                "invalid_request",
177                "code_verifier provided but PKCE was not used in authorization",
178            );
179        }
180        if !verifier.is_empty()
181            && (query.get("code_challenge_method").unwrap_or(&Value::Null) != "S256"
182                || URL_SAFE_NO_PAD.encode(Sha256::digest(verifier)) != challenge)
183        {
184            return error(401, "invalid_request", "code verification failed");
185        }
186        let user_id = value
187            .get("userId")
188            .unwrap_or(&Value::Null)
189            .as_str()
190            .unwrap_or_default();
191        if ctx.database.get_user_by_id(user_id).await?.is_none() {
192            return error(
193                400,
194                "invalid_user",
195                "missing user, user may have been deleted",
196            );
197        }
198        let session_id = value
199            .get("sessionId")
200            .unwrap_or(&Value::Null)
201            .as_str()
202            .unwrap_or_default();
203        let session = ctx
204            .database
205            .get_user_sessions(user_id)
206            .await?
207            .into_iter()
208            .find(|s| s.id().as_ref() == session_id);
209        if session.is_none_or(|s| s.expires_at() < chrono::Utc::now()) {
210            return error(400, "invalid_request", "session no longer exists");
211        }
212        let issued = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
213            .ok_or_else(|| alibi_core::AuthError::internal("Invalid token issue time"))?;
214        let expires = issued + chrono::Duration::seconds(3600);
215
216        let random_token = || {
217            rand::RngExt::sample_iter(rand::rng(), rand::distr::Alphanumeric)
218                .map(char::from)
219                .filter(char::is_ascii_alphabetic)
220                .take(32)
221                .collect::<String>()
222        };
223        let scopes = scope.split(' ').collect::<Vec<_>>();
224        let refresh_allowed = client
225            .metadata
226            .get("grant_types")
227            .and_then(Value::as_array)
228            .is_none_or(|grants| grants.iter().any(|g| g == "refresh_token"));
229        let resources = value
230            .get("resource")
231            .filter(|value| value.as_array().is_none_or(|values| !values.is_empty()))
232            .cloned()
233            .unwrap_or(Value::Null);
234        let refresh = if refresh_allowed && scopes.contains(&"offline_access") {
235            let token = random_token();
236            let auth_time = value
237                .get("authTime")
238                .and_then(Value::as_i64)
239                .and_then(chrono::DateTime::from_timestamp_millis);
240            let row = ctx.database.create_oauth_token(OAuthTokenKind::Refresh,CreateOAuthToken {
241                token:URL_SAFE_NO_PAD.encode(Sha256::digest(&token)),client_id:client_id.clone(),user_id:Some(user_id.into()),session_id:Some(session_id.into()),authorization_code_id:Some(identifier.clone()),
242                metadata:json!({"scopes":scopes,"resources":resources,"authTime":auth_time}),created_at:issued,expires_at:issued+chrono::Duration::seconds(2_592_000)
243            }).await?;
244            Some((row.id, token))
245        } else {
246            None
247        };
248        let token = random_token();
249        let mut metadata = json!({"scopes":scopes,"resources":resources});
250        if let Some((id, _)) = &refresh {
251            _ = metadata
252                .as_object_mut()
253                .ok_or_else(|| alibi_core::AuthError::internal("Invalid access-token metadata"))?
254                .insert("refreshId".into(), json!(id));
255        }
256        _ = ctx
257            .database
258            .create_oauth_token(
259                OAuthTokenKind::Access,
260                CreateOAuthToken {
261                    token: URL_SAFE_NO_PAD.encode(Sha256::digest(&token)),
262                    client_id: client_id.clone(),
263                    user_id: Some(user_id.into()),
264                    session_id: Some(session_id.into()),
265                    authorization_code_id: Some(identifier),
266                    metadata,
267                    created_at: issued,
268                    expires_at: expires,
269                },
270            )
271            .await?;
272        let mut response = json!({"access_token":token,"expires_in":3600,"expires_at":expires.timestamp(),"token_type":"Bearer","scope":scope});
273        if scopes.contains(&"openid") {
274            match Box::pin(self.issue_id_token(&client, &value, &token, req, ctx)).await? {
275                Ok(Some(token)) => {
276                    _ = response
277                        .as_object_mut()
278                        .ok_or_else(|| alibi_core::AuthError::internal("Invalid token response"))?
279                        .insert("id_token".into(), json!(token));
280                }
281                Ok(None) => {}
282                Err(response) => {
283                    return Ok(response
284                        .with_header("Cache-Control", "no-store")
285                        .with_header("Pragma", "no-cache"));
286                }
287            }
288        }
289        if let Some((_, token)) = refresh {
290            _ = response
291                .as_object_mut()
292                .ok_or_else(|| alibi_core::AuthError::internal("Invalid token response"))?
293                .insert("refresh_token".into(), json!(token));
294        }
295        Ok(EndpointResponse::json(&response)?
296            .with_header("Cache-Control", "no-store")
297            .with_header("Pragma", "no-cache"))
298    }
299}
300fn error(status: u16, code: &str, description: &str) -> AuthResult<EndpointResponse> {
301    super::clients::status_protocol_error(status, code, description).map(|r| {
302        r.with_header("Cache-Control", "no-store")
303            .with_header("Pragma", "no-cache")
304    })
305}