shield_oidc/
provider.rs

1use bon::Builder;
2use openidconnect::{
3    AuthUrl, Client, ClientId, ClientSecret, EmptyAdditionalClaims, EndpointMaybeSet,
4    EndpointNotSet, EndpointSet, IntrospectionUrl, IssuerUrl, JsonWebKeySet, JsonWebKeySetUrl,
5    RedirectUrl, RevocationUrl, StandardErrorResponse, TokenUrl, UserInfoUrl,
6    core::{
7        CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreErrorResponseType, CoreGenderClaim,
8        CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm,
9        CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse,
10        CoreTokenResponse,
11    },
12};
13use secrecy::{ExposeSecret, SecretString};
14use shield::{ConfigurationError, Provider};
15
16use crate::{
17    client::async_http_client,
18    metadata::{NonStandardProviderMetadata, OidcProviderMetadata},
19    method::OIDC_METHOD_ID,
20};
21
22type OidcClient = Client<
23    EmptyAdditionalClaims,
24    CoreAuthDisplay,
25    CoreGenderClaim,
26    CoreJweContentEncryptionAlgorithm,
27    CoreJsonWebKey,
28    CoreAuthPrompt,
29    StandardErrorResponse<CoreErrorResponseType>,
30    CoreTokenResponse,
31    CoreTokenIntrospectionResponse,
32    CoreRevocableToken,
33    CoreRevocationErrorResponse,
34    EndpointSet,
35    EndpointNotSet,
36    EndpointNotSet,
37    EndpointNotSet,
38    EndpointMaybeSet,
39    EndpointMaybeSet,
40>;
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum OidcProviderVisibility {
44    Public,
45    Unlisted,
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum OidcProviderPkceCodeChallenge {
50    None,
51    Plain,
52    S256,
53}
54#[expect(clippy::duplicated_attributes)]
55#[derive(Builder, Clone, Debug)]
56#[builder(
57    on(String, into),
58    on(SecretString, into),
59    state_mod(vis = "pub(crate)")
60)]
61pub struct OidcProvider {
62    pub id: String,
63    pub name: String,
64    pub slug: Option<String>,
65    pub icon_url: Option<String>,
66    #[builder(default = OidcProviderVisibility::Public)]
67    pub visibility: OidcProviderVisibility,
68    pub client_id: String,
69    pub client_secret: Option<SecretString>,
70    pub scopes: Option<Vec<String>>,
71    pub redirect_url: Option<String>,
72    pub discovery_url: Option<String>,
73    pub issuer_url: Option<String>,
74    pub authorization_url: Option<String>,
75    pub authorization_url_params: Option<String>,
76    pub token_url: Option<String>,
77    pub token_url_params: Option<String>,
78    pub introspection_url: Option<String>,
79    pub introspection_url_params: Option<String>,
80    pub revocation_url: Option<String>,
81    pub revocation_url_params: Option<String>,
82    pub user_info_url: Option<String>,
83    pub json_web_key_set_url: Option<String>,
84    pub json_web_key_set: Option<JsonWebKeySet<CoreJsonWebKey>>,
85    #[builder(default = OidcProviderPkceCodeChallenge::S256)]
86    pub pkce_code_challenge: OidcProviderPkceCodeChallenge,
87}
88
89impl OidcProvider {
90    pub async fn oidc_client(&self) -> Result<OidcClient, ConfigurationError> {
91        let async_http_client = async_http_client()?;
92
93        let provider_metadata = if let Some(discovery_url) = &self.discovery_url {
94            OidcProviderMetadata::discover_async(
95                // TODO: Consider stripping `/.well-known/openid-configuration` so `openidconnect` doesn't error.
96                IssuerUrl::new(discovery_url.clone())
97                    .map_err(|err| ConfigurationError::Invalid(err.to_string()))?,
98                &async_http_client,
99            )
100            .await
101            .map_err(|err| ConfigurationError::Invalid(err.to_string()))?
102        } else {
103            let mut provider_metadata = OidcProviderMetadata::new(
104                IssuerUrl::new(
105                    self.issuer_url
106                        .clone()
107                        .ok_or(ConfigurationError::Missing("issuer URL".to_owned()))?,
108                )
109                .map_err(|err| ConfigurationError::Invalid(err.to_string()))?,
110                self.authorization_url
111                    .as_ref()
112                    .ok_or(ConfigurationError::Missing("authorization URL".to_owned()))
113                    .and_then(|authorization_url| {
114                        AuthUrl::new(authorization_url.clone())
115                            .map_err(|err| ConfigurationError::Invalid(err.to_string()))
116                    })?,
117                // Dummy URL, never requested.
118                JsonWebKeySetUrl::new("http://127.0.0.1/never-requested".to_owned())
119                    .expect("Valid URL."),
120                vec![],
121                vec![],
122                // By default, signing algorithm is not checked, so allowing all possible values should behave the same.
123                vec![
124                    CoreJwsSigningAlgorithm::HmacSha256,
125                    CoreJwsSigningAlgorithm::HmacSha384,
126                    CoreJwsSigningAlgorithm::HmacSha512,
127                    CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
128                    CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384,
129                    CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512,
130                    CoreJwsSigningAlgorithm::EcdsaP256Sha256,
131                    CoreJwsSigningAlgorithm::EcdsaP384Sha384,
132                    CoreJwsSigningAlgorithm::EcdsaP521Sha512,
133                    CoreJwsSigningAlgorithm::RsaSsaPssSha256,
134                    CoreJwsSigningAlgorithm::RsaSsaPssSha384,
135                    CoreJwsSigningAlgorithm::RsaSsaPssSha512,
136                    CoreJwsSigningAlgorithm::EdDsa,
137                    CoreJwsSigningAlgorithm::None,
138                ],
139                NonStandardProviderMetadata {
140                    introspection_endpoint: self
141                        .introspection_url
142                        .as_ref()
143                        .map(|introspection_url| {
144                            IntrospectionUrl::new(introspection_url.clone())
145                                .map_err(|err| ConfigurationError::Invalid(err.to_string()))
146                        })
147                        .transpose()?,
148                    revocation_endpoint: self
149                        .revocation_url
150                        .as_ref()
151                        .map(|revocation_url| {
152                            RevocationUrl::new(revocation_url.clone())
153                                .map_err(|err| ConfigurationError::Invalid(err.to_string()))
154                        })
155                        .transpose()?,
156                },
157            );
158
159            provider_metadata = provider_metadata.set_jwks(
160                self.json_web_key_set
161                    .clone()
162                    .ok_or(ConfigurationError::Missing("JSON Web Key Set".to_owned()))?,
163            );
164
165            if let Some(token_url) = &self.token_url {
166                provider_metadata = provider_metadata.set_token_endpoint(Some(
167                    TokenUrl::new(token_url.clone())
168                        .map_err(|err| ConfigurationError::Invalid(err.to_string()))?,
169                ));
170            }
171
172            if let Some(user_info_url) = &self.user_info_url {
173                provider_metadata = provider_metadata.set_userinfo_endpoint(Some(
174                    UserInfoUrl::new(user_info_url.clone())
175                        .map_err(|err| ConfigurationError::Invalid(err.to_string()))?,
176                ));
177            }
178
179            provider_metadata
180        };
181
182        let mut client = CoreClient::from_provider_metadata(
183            provider_metadata,
184            ClientId::new(self.client_id.clone()),
185            self.client_secret
186                .clone()
187                .map(|client_secret| ClientSecret::new(client_secret.expose_secret().to_owned())),
188        );
189
190        // TODO: Upstream: _option version of these (and other) functions which set the type to EndpointMaybeSet.
191
192        // if let Some(introspection_endpoint) = provider_metadata
193        //     .additional_metadata()
194        //     .introspection_endpoint
195        // {
196        //     client = client.set_introspection_url(introspection_endpoint);
197        // }
198        // if let Some(revocation_url) = provider_metadata.additional_metadata().revocation_endpoint {
199        //     client = client.set_introspection_url(revocation_url);
200        // }
201
202        if let Some(redirect_url) = &self.redirect_url {
203            client = client.set_redirect_uri(
204                RedirectUrl::new(redirect_url.clone())
205                    .map_err(|err| ConfigurationError::Invalid(err.to_string()))?,
206            );
207        }
208
209        // TODO: Client options.
210
211        Ok(client)
212    }
213}
214
215impl Provider for OidcProvider {
216    fn method_id(&self) -> String {
217        OIDC_METHOD_ID.to_owned()
218    }
219
220    fn id(&self) -> Option<String> {
221        Some(self.id.clone())
222    }
223
224    fn name(&self) -> String {
225        self.name.clone()
226    }
227}