Skip to main content

rws_server/sso/
config.rs

1//! OIDC client configuration and provider presets.
2//!
3//! [`OidcConfig`] bundles the OAuth 2.0 / OIDC parameters for a specific
4//! application registration with an identity provider.  Create one via a
5//! provider preset (e.g. [`OidcConfig::google`]) or load from environment
6//! variables with [`OidcConfig::from_env`].
7
8use super::{discovery::OidcProvider, SsoError};
9
10/// All parameters needed to perform an OIDC / OAuth 2.0 authorization flow.
11#[derive(Clone)]
12pub struct OidcConfig {
13    /// Provider endpoint URLs.
14    pub provider:            OidcProvider,
15    /// OAuth client ID.
16    pub client_id:           String,
17    /// OAuth client secret (empty for PKCE-only public clients).
18    pub client_secret:       String,
19    /// Redirect URI registered with the identity provider.
20    pub redirect_uri:        String,
21    /// Requested OAuth scopes.
22    pub scopes:              Vec<String>,
23    /// Path to redirect to after a successful login.
24    pub post_login_redirect: String,
25}
26
27impl OidcConfig {
28    /// Google OIDC configuration.
29    pub fn google(client_id: &str, client_secret: &str, redirect_uri: &str) -> Self {
30        OidcConfig {
31            provider:            OidcProvider::google(),
32            client_id:           client_id.into(),
33            client_secret:       client_secret.into(),
34            redirect_uri:        redirect_uri.into(),
35            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
36            post_login_redirect: "/".into(),
37        }
38    }
39
40    /// Microsoft Entra ID / Azure AD configuration.
41    ///
42    /// `tenant_id` can be a GUID, `"common"`, `"organizations"`, or `"consumers"`.
43    pub fn microsoft(
44        tenant_id: &str,
45        client_id: &str,
46        client_secret: &str,
47        redirect_uri: &str,
48    ) -> Self {
49        OidcConfig {
50            provider:            OidcProvider::microsoft(tenant_id),
51            client_id:           client_id.into(),
52            client_secret:       client_secret.into(),
53            redirect_uri:        redirect_uri.into(),
54            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
55            post_login_redirect: "/".into(),
56        }
57    }
58
59    /// GitHub OAuth 2.0 configuration.
60    pub fn github(client_id: &str, client_secret: &str, redirect_uri: &str) -> Self {
61        OidcConfig {
62            provider:            OidcProvider::github(),
63            client_id:           client_id.into(),
64            client_secret:       client_secret.into(),
65            redirect_uri:        redirect_uri.into(),
66            scopes:              vec!["read:user".into(), "user:email".into()],
67            post_login_redirect: "/".into(),
68        }
69    }
70
71    /// Okta configuration.
72    ///
73    /// `domain` is your Okta org URL, e.g. `"dev-12345.okta.com"`.
74    pub fn okta(domain: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> Self {
75        OidcConfig {
76            provider:            OidcProvider::okta(domain),
77            client_id:           client_id.into(),
78            client_secret:       client_secret.into(),
79            redirect_uri:        redirect_uri.into(),
80            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
81            post_login_redirect: "/".into(),
82        }
83    }
84
85    /// Auth0 configuration.
86    ///
87    /// `domain` is your Auth0 domain, e.g. `"myapp.us.auth0.com"`.
88    pub fn auth0(domain: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> Self {
89        OidcConfig {
90            provider:            OidcProvider::auth0(domain),
91            client_id:           client_id.into(),
92            client_secret:       client_secret.into(),
93            redirect_uri:        redirect_uri.into(),
94            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
95            post_login_redirect: "/".into(),
96        }
97    }
98
99    /// Keycloak configuration.
100    pub fn keycloak(
101        base_url: &str,
102        realm: &str,
103        client_id: &str,
104        client_secret: &str,
105        redirect_uri: &str,
106    ) -> Self {
107        OidcConfig {
108            provider:            OidcProvider::keycloak(base_url, realm),
109            client_id:           client_id.into(),
110            client_secret:       client_secret.into(),
111            redirect_uri:        redirect_uri.into(),
112            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
113            post_login_redirect: "/".into(),
114        }
115    }
116
117    /// Discover a custom OIDC provider via `{issuer}/.well-known/openid-configuration`.
118    pub fn discover(
119        issuer: &str,
120        client_id: &str,
121        client_secret: &str,
122        redirect_uri: &str,
123    ) -> Result<Self, SsoError> {
124        Ok(OidcConfig {
125            provider:            OidcProvider::discover(issuer)?,
126            client_id:           client_id.into(),
127            client_secret:       client_secret.into(),
128            redirect_uri:        redirect_uri.into(),
129            scopes:              vec!["openid".into(), "email".into(), "profile".into()],
130            post_login_redirect: "/".into(),
131        })
132    }
133
134    /// Load configuration from environment variables.
135    ///
136    /// Required variables:
137    /// - `RWS_OIDC_PROVIDER` — `google`, `github`, `microsoft`, `okta`, `auth0`, `keycloak`, or `custom`
138    /// - `RWS_OIDC_CLIENT_ID`
139    /// - `RWS_OIDC_REDIRECT_URI`
140    ///
141    /// Optional variables:
142    /// - `RWS_OIDC_CLIENT_SECRET` (default: empty)
143    /// - `RWS_OIDC_ISSUER` — required for `okta`, `auth0`, `keycloak`, `custom`
144    /// - `RWS_OIDC_TENANT_ID` — required for `microsoft` (tenant) and `keycloak` (realm)
145    /// - `RWS_OIDC_SCOPES` — space-separated (default: `openid email profile`)
146    /// - `RWS_OIDC_POST_LOGIN_REDIRECT` (default: `/`)
147    pub fn from_env() -> Result<Self, SsoError> {
148        let provider_name = std::env::var("RWS_OIDC_PROVIDER")
149            .map_err(|_| SsoError("RWS_OIDC_PROVIDER not set".into()))?;
150        let client_id = std::env::var("RWS_OIDC_CLIENT_ID")
151            .map_err(|_| SsoError("RWS_OIDC_CLIENT_ID not set".into()))?;
152        let client_secret = std::env::var("RWS_OIDC_CLIENT_SECRET").unwrap_or_default();
153        let redirect_uri = std::env::var("RWS_OIDC_REDIRECT_URI")
154            .map_err(|_| SsoError("RWS_OIDC_REDIRECT_URI not set".into()))?;
155        let post_login_redirect =
156            std::env::var("RWS_OIDC_POST_LOGIN_REDIRECT").unwrap_or_else(|_| "/".into());
157        let scopes: Vec<String> = std::env::var("RWS_OIDC_SCOPES")
158            .unwrap_or_else(|_| "openid email profile".into())
159            .split_whitespace()
160            .map(String::from)
161            .collect();
162
163        let provider = match provider_name.as_str() {
164            "google" => OidcProvider::google(),
165            "github" => OidcProvider::github(),
166            "microsoft" => {
167                let tenant = std::env::var("RWS_OIDC_TENANT_ID")
168                    .map_err(|_| SsoError("RWS_OIDC_TENANT_ID required for microsoft".into()))?;
169                OidcProvider::microsoft(&tenant)
170            }
171            "okta" => {
172                let domain = std::env::var("RWS_OIDC_ISSUER")
173                    .map_err(|_| SsoError("RWS_OIDC_ISSUER required for okta".into()))?;
174                OidcProvider::okta(&domain)
175            }
176            "auth0" => {
177                let domain = std::env::var("RWS_OIDC_ISSUER")
178                    .map_err(|_| SsoError("RWS_OIDC_ISSUER required for auth0".into()))?;
179                OidcProvider::auth0(&domain)
180            }
181            "keycloak" => {
182                let base = std::env::var("RWS_OIDC_ISSUER").map_err(|_| {
183                    SsoError("RWS_OIDC_ISSUER required for keycloak (base_url)".into())
184                })?;
185                let realm = std::env::var("RWS_OIDC_TENANT_ID").map_err(|_| {
186                    SsoError("RWS_OIDC_TENANT_ID required for keycloak (realm)".into())
187                })?;
188                OidcProvider::keycloak(&base, &realm)
189            }
190            _ => {
191                // "custom" or any unrecognised value
192                let issuer = std::env::var("RWS_OIDC_ISSUER")
193                    .map_err(|_| SsoError("RWS_OIDC_ISSUER required for custom provider".into()))?;
194                OidcProvider::discover(&issuer)?
195            }
196        };
197
198        Ok(OidcConfig {
199            provider,
200            client_id,
201            client_secret,
202            redirect_uri,
203            scopes,
204            post_login_redirect,
205        })
206    }
207
208    /// Override the post-login redirect path (default: `"/"`).
209    pub fn post_login_redirect(mut self, path: &str) -> Self {
210        self.post_login_redirect = path.into();
211        self
212    }
213
214    /// Override the requested scopes.
215    pub fn scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
216        self.scopes = scopes.into_iter().map(Into::into).collect();
217        self
218    }
219}