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}