aster_forge_external_auth/
driver.rs

1//! Provider driver trait and shared external-auth flow value objects.
2//!
3//! The driver contract is intentionally expressed in runtime DTOs rather than application database
4//! models. Product crates adapt their stored provider rows into [`ExternalAuthProviderConfig`],
5//! then call a registered driver to start authorization, exchange callbacks, or validate provider
6//! configuration.
7
8use crate::types::{ExternalAuthProtocol, ExternalAuthProviderKind, ExternalAuthProviderOptions};
9use crate::{ExternalAuthError, Result};
10use async_trait::async_trait;
11use serde::Serialize;
12use std::fmt;
13
14/// Static metadata that describes a provider driver's capabilities and configuration needs.
15#[expect(
16    clippy::struct_excessive_bools,
17    reason = "These booleans are independent provider capabilities and configuration requirements exposed to registry consumers."
18)]
19#[derive(Clone, Debug)]
20pub struct ExternalAuthProviderDescriptor {
21    /// Provider kind handled by the driver.
22    pub kind: ExternalAuthProviderKind,
23    /// Protocol family used by the driver.
24    pub protocol: ExternalAuthProtocol,
25    /// Human-readable provider name.
26    pub display_name: &'static str,
27    /// Short capability summary suitable for admin UI surfaces.
28    pub description: &'static str,
29    /// Default scopes used when a provider config leaves scopes empty.
30    pub default_scopes: &'static str,
31    /// Whether an issuer URL must be supplied by administrators.
32    pub issuer_url_required: bool,
33    /// Whether administrators may manually configure OAuth/OIDC endpoints.
34    pub manual_endpoint_configuration_supported: bool,
35    /// Whether the authorization endpoint is required.
36    pub authorization_url_required: bool,
37    /// Whether the token endpoint is required.
38    pub token_url_required: bool,
39    /// Whether the userinfo endpoint is required.
40    pub userinfo_url_required: bool,
41    /// Whether provider discovery is supported.
42    pub supports_discovery: bool,
43    /// Whether the authorization flow uses PKCE.
44    pub supports_pkce: bool,
45    /// Whether profile extraction can use an email-verified claim.
46    pub supports_email_verified_claim: bool,
47}
48
49/// Runtime configuration used by provider drivers.
50///
51/// The value is intentionally independent from persistence. Application crates can keep their own
52/// schema, encrypted secret handling, `OpenAPI` shape, and migration behavior, then construct this
53/// config immediately before invoking a provider driver.
54#[derive(Clone)]
55pub struct ExternalAuthProviderConfig {
56    /// Product-owned provider id, carried through for logging and app-level correlation.
57    pub id: i64,
58    /// Product-owned stable provider key.
59    pub key: String,
60    /// Provider kind selected by the application.
61    pub provider_kind: ExternalAuthProviderKind,
62    /// Protocol selected by the application.
63    pub protocol: ExternalAuthProtocol,
64    /// Connector-specific decoded options.
65    pub options: ExternalAuthProviderOptions,
66    /// Issuer URL for OIDC-style providers.
67    pub issuer_url: Option<String>,
68    /// Authorization endpoint for manual `OAuth2` providers.
69    pub authorization_url: Option<String>,
70    /// Token endpoint for manual `OAuth2` providers.
71    pub token_url: Option<String>,
72    /// Userinfo endpoint for manual `OAuth2` providers.
73    pub userinfo_url: Option<String>,
74    /// OAuth/OIDC client id.
75    pub client_id: String,
76    /// Optional OAuth/OIDC client secret.
77    pub client_secret: Option<String>,
78    /// Space-separated scope list.
79    pub scopes: String,
80    /// Optional profile claim name or JSON pointer used as the subject.
81    pub subject_claim: Option<String>,
82    /// Optional profile claim name or JSON pointer used as the preferred username.
83    pub username_claim: Option<String>,
84    /// Optional profile claim name or JSON pointer used as the display name.
85    pub display_name_claim: Option<String>,
86    /// Optional profile claim name or JSON pointer used as the email address.
87    pub email_claim: Option<String>,
88    /// Optional profile claim name or JSON pointer used as the email verification flag.
89    pub email_verified_claim: Option<String>,
90    /// Optional profile claim name reserved for group extraction by application crates.
91    pub groups_claim: Option<String>,
92    /// Optional profile claim name reserved for avatar URL extraction by application crates.
93    pub avatar_url_claim: Option<String>,
94    /// Optional application-owned User-Agent for outbound provider API requests.
95    pub outbound_http_user_agent: Option<String>,
96}
97
98impl fmt::Debug for ExternalAuthProviderConfig {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("ExternalAuthProviderConfig")
101            .field("id", &self.id)
102            .field("key", &self.key)
103            .field("provider_kind", &self.provider_kind)
104            .field("protocol", &self.protocol)
105            .field("options", &self.options)
106            .field("issuer_url", &self.issuer_url)
107            .field("authorization_url", &self.authorization_url)
108            .field("token_url", &self.token_url)
109            .field("userinfo_url", &self.userinfo_url)
110            .field("client_id", &self.client_id)
111            .field(
112                "client_secret",
113                &self.client_secret.as_ref().map(|_| "***REDACTED***"),
114            )
115            .field("scopes", &self.scopes)
116            .field("subject_claim", &self.subject_claim)
117            .field("username_claim", &self.username_claim)
118            .field("display_name_claim", &self.display_name_claim)
119            .field("email_claim", &self.email_claim)
120            .field("email_verified_claim", &self.email_verified_claim)
121            .field("groups_claim", &self.groups_claim)
122            .field("avatar_url_claim", &self.avatar_url_claim)
123            .field("outbound_http_user_agent", &self.outbound_http_user_agent)
124            .finish()
125    }
126}
127
128impl ExternalAuthProviderConfig {
129    /// Returns a non-empty issuer URL or a validation error.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`ExternalAuthError`] when the provider configuration has no issuer URL.
134    pub fn require_issuer_url(&self) -> Result<&str> {
135        self.issuer_url
136            .as_deref()
137            .filter(|value| !value.trim().is_empty())
138            .ok_or_else(|| {
139                ExternalAuthError::validation_error("external auth provider missing issuer_url")
140            })
141    }
142}
143
144/// Result of starting an authorization flow.
145#[derive(Clone, Debug)]
146pub struct ExternalAuthAuthorizationStart {
147    /// Provider authorization URL to redirect the browser to.
148    pub authorization_url: String,
149    /// CSRF state stored by the application for callback validation.
150    pub state: String,
151    /// Optional OIDC nonce stored by the application for callback validation.
152    pub nonce: Option<String>,
153    /// Optional PKCE verifier stored by the application for callback exchange.
154    pub pkce_verifier: Option<String>,
155}
156
157/// Callback payload needed by a provider driver to exchange an authorization code.
158#[derive(Clone, Debug)]
159pub struct ExternalAuthCallback {
160    /// Authorization code returned by the provider.
161    pub code: String,
162    /// Stored OIDC nonce, when the provider uses one.
163    pub nonce: Option<String>,
164    /// Stored PKCE verifier.
165    pub pkce_verifier: Option<String>,
166    /// Redirect URI used for this login flow.
167    pub redirect_uri: String,
168}
169
170/// Normalized profile returned by an external authentication provider.
171#[derive(Clone, Debug)]
172pub struct ExternalAuthProfile {
173    /// Provider-scoped namespace used to avoid subject collisions across issuers and connectors.
174    pub identity_namespace: String,
175    /// Provider subject identifier.
176    pub subject: String,
177    /// Optional email address.
178    pub email: Option<String>,
179    /// Whether the provider asserted the email as verified.
180    pub email_verified: bool,
181    /// Optional display name snapshot.
182    pub display_name: Option<String>,
183    /// Optional preferred username snapshot.
184    pub preferred_username: Option<String>,
185}
186
187/// Single provider health/test check.
188#[derive(Clone, Debug, Serialize)]
189#[cfg_attr(all(debug_assertions, feature = "openapi"), derive(utoipa::ToSchema))]
190pub struct ExternalAuthProviderTestCheck {
191    /// Machine-readable check name.
192    pub name: String,
193    /// Whether the check passed.
194    pub success: bool,
195    /// Human-readable check result.
196    pub message: String,
197}
198
199/// Provider health/test result returned to admin tooling.
200#[derive(Clone, Debug, Serialize)]
201#[cfg_attr(all(debug_assertions, feature = "openapi"), derive(utoipa::ToSchema))]
202pub struct ExternalAuthProviderTestResult {
203    /// Display name of the tested provider driver.
204    pub provider: String,
205    /// Effective issuer URL, when applicable.
206    pub issuer: Option<String>,
207    /// Effective authorization endpoint, when applicable.
208    pub authorization_endpoint: Option<String>,
209    /// Effective token endpoint, when applicable.
210    pub token_endpoint: Option<String>,
211    /// Effective userinfo endpoint, when applicable.
212    pub userinfo_endpoint: Option<String>,
213    /// Number of discovered JWKS keys, when applicable.
214    pub jwks_key_count: Option<usize>,
215    /// Individual test checks.
216    pub checks: Vec<ExternalAuthProviderTestCheck>,
217}
218
219/// External authentication provider driver.
220#[async_trait]
221pub trait ExternalAuthProviderDriver: Send + Sync {
222    /// Returns the provider kind handled by this driver.
223    fn kind(&self) -> ExternalAuthProviderKind;
224
225    /// Returns static provider metadata and capability flags.
226    fn descriptor(&self) -> ExternalAuthProviderDescriptor;
227
228    /// Builds an authorization URL and state values for a browser redirect.
229    async fn start_authorization(
230        &self,
231        provider: &ExternalAuthProviderConfig,
232        redirect_uri: &str,
233    ) -> Result<ExternalAuthAuthorizationStart>;
234
235    /// Exchanges a callback authorization code and returns a normalized profile.
236    async fn exchange_callback(
237        &self,
238        provider: &ExternalAuthProviderConfig,
239        callback: ExternalAuthCallback,
240    ) -> Result<ExternalAuthProfile>;
241
242    /// Performs configuration/discovery checks suitable for admin validation.
243    async fn test_provider(
244        &self,
245        provider: &ExternalAuthProviderConfig,
246    ) -> Result<ExternalAuthProviderTestResult>;
247}