Skip to main content

alibi_plugins/oauth_provider/
post_login.rs

1use alibi_core::{AuthResult, SessionView, UserView};
2use async_trait::async_trait;
3use std::{collections::HashMap, sync::Arc};
4
5/// Authenticated post-login policy input. The pinned callback does not receive
6/// a client argument; admission has already validated the requesting client.
7pub struct OAuthInteractionContext<'a> {
8    pub headers: &'a HashMap<String, String>,
9    pub user: &'a UserView,
10    pub session: &'a SessionView,
11    pub scopes: &'a [String],
12}
13
14/// Post-login policy input: the interaction context plus whether the user is
15/// returning from the post-login page.
16pub struct OAuthPostLoginContext<'a> {
17    pub headers: &'a HashMap<String, String>,
18    pub user: &'a UserView,
19    pub session: &'a SessionView,
20    pub scopes: &'a [String],
21    /// `true` when the user returns from the post-login page through
22    /// `/oauth2/continue`. The client chooses to send that request, so it does
23    /// not prove a selection was made: validate the session before returning
24    /// `false`.
25    pub is_continuation: bool,
26}
27
28#[async_trait]
29pub trait OAuthPostLoginPolicy: Send + Sync + std::fmt::Debug {
30    async fn should_redirect(&self, input: OAuthPostLoginContext<'_>) -> AuthResult<bool>;
31}
32
33/// Consent authority selection after post-login interaction. No request headers
34/// or client are supplied by the pinned callback contract.
35pub struct OAuthConsentReferenceContext<'a> {
36    pub user: &'a UserView,
37    pub session: &'a SessionView,
38    pub scopes: &'a [String],
39}
40
41#[async_trait]
42pub trait OAuthConsentReference: Send + Sync + std::fmt::Debug {
43    async fn consent_reference_id(
44        &self,
45        input: OAuthConsentReferenceContext<'_>,
46    ) -> AuthResult<Option<String>>;
47}
48
49/// Application interaction after signup policy and before consent.
50#[derive(Debug, Clone)]
51pub struct OAuthPostLoginConfig {
52    pub page: String,
53    pub should_redirect: Arc<dyn OAuthPostLoginPolicy>,
54    /// Select an additional consent partition within the authenticated user's
55    /// grants. Consent is stored per reference; `None` or an empty returned
56    /// string uses consent given without one. Neither changes user ownership of
57    /// consent-management operations.
58    pub consent_reference_id: Option<Arc<dyn OAuthConsentReference>>,
59}
60
61impl super::OAuthProviderPlugin {
62    pub(super) async fn consent_reference_id(
63        &self,
64        user: &UserView,
65        session: &SessionView,
66        scopes: &[String],
67    ) -> AuthResult<Option<String>> {
68        let Some(reference) = self
69            .config
70            .post_login
71            .as_ref()
72            .and_then(|config| config.consent_reference_id.as_ref())
73        else {
74            return Ok(None);
75        };
76        reference
77            .consent_reference_id(OAuthConsentReferenceContext {
78                user,
79                session,
80                scopes,
81            })
82            .await
83            .map(|reference| reference.filter(|reference| !reference.is_empty()))
84            .map_err(|error| {
85                if alibi_core::endpoint::is_endpoint_api_error(&error) {
86                    error
87                } else {
88                    alibi_core::AuthError::CallbackFailure(Box::new(error))
89                }
90            })
91    }
92}
93/// Whether stored consent belongs to exactly this reference. Without one, only
94/// consent stored without a reference (missing, null or empty) matches.
95pub(super) fn consent_matches_reference(
96    metadata: &serde_json::Value,
97    reference: Option<&str>,
98) -> bool {
99    let stored = metadata
100        .get("referenceId")
101        .and_then(serde_json::Value::as_str)
102        .filter(|stored| !stored.is_empty());
103    stored == reference
104}