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 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 JsonWebKeySetUrl::new("http://127.0.0.1/never-requested".to_owned())
119 .expect("Valid URL."),
120 vec![],
121 vec![],
122 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 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 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}