Skip to main content

alibi_plugins/oauth_provider/
continuation.rs

1//! Resume provider-issued authorization state after an authenticated interaction.
2use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{EndpointResponse, ServerEndpoint};
4use alibi_core::session::SessionRequest;
5use alibi_core::utils::json::JsValue;
6use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema};
7use serde_json::{Value, json};
8#[derive(serde::Deserialize)]
9pub(super) struct ContinuationInput {
10    selected: Option<bool>,
11    created: Option<bool>,
12    #[serde(rename = "postLogin")]
13    post_login: Option<bool>,
14    oauth_query: Option<String>,
15}
16pub(super) fn validate(input: Option<&JsValue>) -> AuthResult<JsValue> {
17    use crate::authentication_helpers::{JsonField, JsonFieldKind};
18    crate::endpoint::validate_fields(
19        input,
20        "body",
21        &[
22            JsonField {
23                name: "selected",
24                kind: JsonFieldKind::Boolean,
25                required: false,
26            },
27            JsonField {
28                name: "created",
29                kind: JsonFieldKind::Boolean,
30                required: false,
31            },
32            JsonField {
33                name: "postLogin",
34                kind: JsonFieldKind::Boolean,
35                required: false,
36            },
37            JsonField::string("oauth_query", false),
38        ],
39    )
40}
41pub(super) fn interaction_query(supplied: &str) -> serde_json::Map<String, Value> {
42    let mut query = serde_json::Map::new();
43    for (key, value) in url::form_urlencoded::parse(supplied.as_bytes()) {
44        if ["sig", "exp", "ba_iat", "ba_pl"].contains(&key.as_ref()) {
45            continue;
46        }
47        match query.entry(key.into_owned()) {
48            serde_json::map::Entry::Vacant(entry) => {
49                _ = entry.insert(json!(value));
50            }
51            serde_json::map::Entry::Occupied(mut entry) => {
52                let previous = entry.get().clone();
53                if let Some(values) = entry.get_mut().as_array_mut() {
54                    values.push(json!(value));
55                } else {
56                    _ = entry.insert(json!([previous, value]));
57                }
58            }
59        }
60    }
61    query
62}
63// Read only after signature admission. Never accept a clearance flag from the
64// ordinary authorization query or from selected/created continuation branches.
65pub(super) fn post_login_clearance(supplied: &str) -> Option<String> {
66    url::form_urlencoded::parse(supplied.as_bytes())
67        .find(|(key, _)| key == "ba_pl")
68        .map(|(_, value)| value.into_owned())
69}
70impl OAuthProviderPlugin {
71    /// Resume a signed OAuth request after account selection, registration or post-login interaction.
72    pub fn oauth2_continue_endpoint(
73        input: &impl serde::Serialize,
74    ) -> AuthResult<ServerEndpoint<Value>> {
75        ServerEndpoint::new("oauth-provider", "oauth2Continue").with_body(input)
76    }
77    pub(super) async fn continue_authorization<S: AuthSchema>(
78        &self,
79        input: ContinuationInput,
80        req: &impl SessionRequest,
81        ctx: &AuthContext<S>,
82    ) -> AuthResult<EndpointResponse> {
83        let supplied = input.oauth_query.as_deref().unwrap_or("");
84        if !supplied.is_empty()
85            && !super::authorization::verify_authorization_query(
86                supplied,
87                ctx.config.current_secret(),
88            )?
89        {
90            return Ok(EndpointResponse::error(AuthError::Api {
91                status: 400,
92                code: None,
93                message: String::new(),
94            })
95            .with_error_body(alibi_core::utils::json::parse_value(
96                r#"{"error":"invalid_signature"}"#,
97            )?));
98        }
99        _ = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
100        let remove = if input.selected == Some(true) {
101            Some("select_account")
102        } else if input.created == Some(true) {
103            Some("create")
104        } else if input.post_login == Some(true) {
105            None
106        } else {
107            return super::clients::status_protocol_error(
108                400,
109                "invalid_request",
110                "Missing parameters",
111            );
112        };
113        if supplied.is_empty() {
114            return super::clients::status_protocol_error(
115                400,
116                "invalid_request",
117                "missing oauth query",
118            );
119        }
120        let mut query = interaction_query(supplied);
121        let clearance = if remove.is_none() {
122            post_login_clearance(supplied)
123        } else {
124            None
125        };
126        if let Some(remove) = remove
127            && let Some(prompt) = query.get("prompt").and_then(Value::as_str)
128        {
129            let mut prompts = prompt.split(' ').collect::<Vec<_>>();
130            if let Some(index) = prompts.iter().position(|prompt| *prompt == remove) {
131                _ = prompts.remove(index);
132                if prompts.is_empty() {
133                    _ = query.remove("prompt");
134                } else {
135                    _ = query.insert("prompt".into(), json!(prompts.join(" ")));
136                }
137            }
138        }
139        Box::pin(self.resume_authorization(query, req, ctx, clearance)).await
140    }
141}