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}