Skip to main content

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