1use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16use openidconnect::core::{
17 CoreIdToken, CoreIdTokenVerifier, CoreJsonWebKeySet, CoreJwsSigningAlgorithm,
18 CoreProviderMetadata,
19};
20use openidconnect::{ClientId, IssuerUrl, JsonWebKeySetUrl, Nonce};
21use tokio::sync::RwLock;
22
23use crate::{Error, Result};
24
25pub const ISSUER_ENV: &str = "OIDC_ISSUER_URL";
27pub const AUDIENCES_ENV: &str = "OIDC_AUDIENCES";
29pub const CUSTOMER_AUDIENCES_ENV: &str = "OIDC_CUSTOMER_AUDIENCES";
32
33pub const SETUP_HINT: &str = "Register your apps with an OpenID Connect provider, then set \
35 OIDC_ISSUER_URL (e.g. https://accounts.google.com) and OIDC_AUDIENCES (their client IDs, \
36 comma-separated) in .env.";
37
38const JWKS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
40
41#[derive(Debug, Clone)]
43pub struct OidcConfig {
44 pub issuer: String,
45 pub audiences: Vec<String>,
47 pub customer_audiences: Vec<String>,
49}
50
51impl OidcConfig {
52 pub fn from_env() -> Result<Self> {
54 let issuer = std::env::var(ISSUER_ENV)
55 .ok()
56 .filter(|issuer| !issuer.trim().is_empty())
57 .ok_or(Error::MissingOidcSetting(ISSUER_ENV))?;
58 let list = |key: &str| -> Vec<String> {
59 std::env::var(key)
60 .unwrap_or_default()
61 .split(',')
62 .map(str::trim)
63 .filter(|audience| !audience.is_empty())
64 .map(str::to_owned)
65 .collect()
66 };
67 let audiences = list(AUDIENCES_ENV);
68 let customer_audiences = list(CUSTOMER_AUDIENCES_ENV);
69 if audiences.is_empty() && customer_audiences.is_empty() {
70 return Err(Error::MissingOidcSetting(AUDIENCES_ENV));
71 }
72 Ok(Self {
73 issuer: issuer.trim().to_owned(),
74 audiences,
75 customer_audiences,
76 })
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Identity {
83 pub issuer: String,
85 pub subject: String,
87 pub email: Option<String>,
88 pub email_verified: Option<bool>,
89 pub name: Option<String>,
90 pub customer: bool,
93}
94
95#[derive(Debug, thiserror::Error)]
97pub enum VerifyError {
98 #[error("invalid ID token: {0}")]
100 Invalid(String),
101 #[error("cannot reach the OpenID Connect provider: {0}")]
103 Unavailable(String),
104}
105
106#[derive(Clone)]
108pub struct OidcVerifier {
109 inner: Arc<Inner>,
110}
111
112struct Inner {
113 issuer: IssuerUrl,
114 audiences: Vec<(ClientId, bool)>,
116 jwks_url: JsonWebKeySetUrl,
117 http: openidconnect::reqwest::Client,
118 jwks: RwLock<(CoreJsonWebKeySet, Instant)>,
119}
120
121impl std::fmt::Debug for OidcVerifier {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("OidcVerifier")
124 .field("issuer", &self.inner.issuer.as_str())
125 .field("audiences", &self.inner.audiences)
126 .finish()
127 }
128}
129
130fn allowed_algs() -> [CoreJwsSigningAlgorithm; 8] {
132 use CoreJwsSigningAlgorithm::*;
133 [
134 RsaSsaPkcs1V15Sha256,
135 RsaSsaPkcs1V15Sha384,
136 RsaSsaPkcs1V15Sha512,
137 RsaSsaPssSha256,
138 RsaSsaPssSha384,
139 EcdsaP256Sha256,
140 EcdsaP384Sha384,
141 EdDsa,
142 ]
143}
144
145impl OidcVerifier {
146 pub async fn discover(config: &OidcConfig) -> Result<Self> {
149 let issuer = IssuerUrl::new(config.issuer.clone())
150 .map_err(|err| Error::Oidc(format!("invalid {ISSUER_ENV}: {err}")))?;
151 let http = openidconnect::reqwest::ClientBuilder::new()
152 .redirect(openidconnect::reqwest::redirect::Policy::none())
153 .build()
154 .map_err(|err| Error::Oidc(format!("HTTP client: {err}")))?;
155 let metadata = CoreProviderMetadata::discover_async(issuer.clone(), &http)
156 .await
157 .map_err(|err| {
158 Error::Oidc(format!("discovery at {} failed: {err}", issuer.as_str()))
159 })?;
160 let jwks_url = metadata.jwks_uri().clone();
161 let jwks = CoreJsonWebKeySet::fetch_async(&jwks_url, &http)
162 .await
163 .map_err(|err| Error::Oidc(format!("fetching signing keys failed: {err}")))?;
164 Ok(Self {
165 inner: Arc::new(Inner {
166 issuer,
167 audiences: config
169 .customer_audiences
170 .iter()
171 .map(|id| (ClientId::new(id.clone()), true))
172 .chain(
173 config
174 .audiences
175 .iter()
176 .map(|id| (ClientId::new(id.clone()), false)),
177 )
178 .collect(),
179 jwks_url,
180 http,
181 jwks: RwLock::new((jwks, Instant::now())),
182 }),
183 })
184 }
185
186 pub fn issuer(&self) -> &str {
188 self.inner.issuer.as_str()
189 }
190
191 pub async fn verify(&self, id_token: &str) -> std::result::Result<Identity, VerifyError> {
193 let token: CoreIdToken = id_token
194 .trim()
195 .parse()
196 .map_err(|err| VerifyError::Invalid(format!("malformed: {err}")))?;
197
198 match self.check(&token).await {
199 Ok(identity) => Ok(identity),
200 Err(err) if self.refresh_keys_if_stale().await? => {
202 self.check(&token).await.map_err(|_| err)
203 }
204 Err(err) => Err(err),
205 }
206 }
207
208 async fn check(&self, token: &CoreIdToken) -> std::result::Result<Identity, VerifyError> {
209 let jwks = self.inner.jwks.read().await.0.clone();
210 let mut last_error = None;
211 for (audience, customer) in &self.inner.audiences {
212 let verifier = CoreIdTokenVerifier::new_public_client(
213 audience.clone(),
214 self.inner.issuer.clone(),
215 jwks.clone(),
216 )
217 .set_allowed_algs(allowed_algs());
218 match token.claims(&verifier, |_: Option<&Nonce>| Ok(())) {
220 Ok(claims) => {
221 return Ok(Identity {
222 issuer: claims.issuer().to_string(),
223 subject: claims.subject().to_string(),
224 email: claims.email().map(|email| email.to_string()),
225 email_verified: claims.email_verified(),
226 name: claims
227 .name()
228 .and_then(|name| name.get(None))
229 .map(|name| name.to_string()),
230 customer: *customer,
231 });
232 }
233 Err(err) => last_error = Some(err.to_string()),
234 }
235 }
236 Err(VerifyError::Invalid(
237 last_error.unwrap_or_else(|| "no audience configured".into()),
238 ))
239 }
240
241 async fn refresh_keys_if_stale(&self) -> std::result::Result<bool, VerifyError> {
243 let mut jwks = self.inner.jwks.write().await;
244 if jwks.1.elapsed() < JWKS_REFRESH_INTERVAL {
245 return Ok(false);
246 }
247 let fresh = CoreJsonWebKeySet::fetch_async(&self.inner.jwks_url, &self.inner.http)
248 .await
249 .map_err(|err| VerifyError::Unavailable(err.to_string()))?;
250 *jwks = (fresh, Instant::now());
251 Ok(true)
252 }
253}
254
255#[derive(Clone)]
265pub struct OidcClient {
266 metadata: CoreProviderMetadata,
267 client_id: ClientId,
268 client_secret: Option<openidconnect::ClientSecret>,
269 http: openidconnect::reqwest::Client,
270}
271
272impl std::fmt::Debug for OidcClient {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("OidcClient")
275 .field("issuer", &self.metadata.issuer().as_str())
276 .field("client_id", &self.client_id.as_str())
277 .finish()
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct PendingSignIn {
286 pub state: String,
287 pub pkce_verifier: String,
288 pub nonce: String,
289}
290
291impl PendingSignIn {
292 pub fn encode(&self) -> String {
294 format!("{}.{}.{}", self.state, self.pkce_verifier, self.nonce)
295 }
296
297 pub fn decode(value: &str) -> Option<Self> {
298 let mut parts = value.split('.');
299 let pending = Self {
300 state: parts.next()?.to_owned(),
301 pkce_verifier: parts.next()?.to_owned(),
302 nonce: parts.next()?.to_owned(),
303 };
304 parts.next().is_none().then_some(pending)
305 }
306}
307
308impl OidcClient {
309 pub async fn discover(
312 issuer: &str,
313 client_id: &str,
314 client_secret: Option<&str>,
315 ) -> Result<Self> {
316 let issuer = IssuerUrl::new(issuer.to_owned())
317 .map_err(|err| Error::Oidc(format!("invalid issuer URL: {err}")))?;
318 let http = openidconnect::reqwest::ClientBuilder::new()
319 .redirect(openidconnect::reqwest::redirect::Policy::none())
320 .build()
321 .map_err(|err| Error::Oidc(format!("HTTP client: {err}")))?;
322 let metadata = CoreProviderMetadata::discover_async(issuer.clone(), &http)
323 .await
324 .map_err(|err| {
325 Error::Oidc(format!("discovery at {} failed: {err}", issuer.as_str()))
326 })?;
327 Ok(Self {
328 metadata,
329 client_id: ClientId::new(client_id.to_owned()),
330 client_secret: client_secret
331 .filter(|secret| !secret.is_empty())
332 .map(|secret| openidconnect::ClientSecret::new(secret.to_owned())),
333 http,
334 })
335 }
336
337 pub fn authorize_url(&self, redirect_url: &str) -> Result<(String, PendingSignIn)> {
340 self.authorize_url_with(redirect_url, &[])
341 }
342
343 pub fn authorize_url_with(
346 &self,
347 redirect_url: &str,
348 extra: &[(&str, &str)],
349 ) -> Result<(String, PendingSignIn)> {
350 let client = openidconnect::core::CoreClient::from_provider_metadata(
351 self.metadata.clone(),
352 self.client_id.clone(),
353 self.client_secret.clone(),
354 )
355 .set_redirect_uri(
356 openidconnect::RedirectUrl::new(redirect_url.to_owned())
357 .map_err(|err| Error::Oidc(format!("invalid redirect URL: {err}")))?,
358 );
359 let (challenge, verifier) = openidconnect::PkceCodeChallenge::new_random_sha256();
360 let mut request = client
361 .authorize_url(
362 openidconnect::core::CoreAuthenticationFlow::AuthorizationCode,
363 openidconnect::CsrfToken::new_random,
364 Nonce::new_random,
365 )
366 .add_scope(openidconnect::Scope::new("email".into()))
367 .add_scope(openidconnect::Scope::new("profile".into()))
368 .set_pkce_challenge(challenge);
369 for (name, value) in extra {
370 request = request.add_extra_param(*name, *value);
371 }
372 let (url, state, nonce) = request.url();
373 Ok((
374 url.to_string(),
375 PendingSignIn {
376 state: state.secret().clone(),
377 pkce_verifier: verifier.secret().clone(),
378 nonce: nonce.secret().clone(),
379 },
380 ))
381 }
382
383 pub async fn exchange(
386 &self,
387 redirect_url: &str,
388 code: &str,
389 state: &str,
390 pending: &PendingSignIn,
391 ) -> Result<String> {
392 if state != pending.state {
393 return Err(Error::Oidc(
394 "sign-in state does not match; start again".into(),
395 ));
396 }
397 let client = openidconnect::core::CoreClient::from_provider_metadata(
398 self.metadata.clone(),
399 self.client_id.clone(),
400 self.client_secret.clone(),
401 )
402 .set_redirect_uri(
403 openidconnect::RedirectUrl::new(redirect_url.to_owned())
404 .map_err(|err| Error::Oidc(format!("invalid redirect URL: {err}")))?,
405 );
406 let response = client
407 .exchange_code(openidconnect::AuthorizationCode::new(code.to_owned()))
408 .map_err(|err| Error::Oidc(format!("token endpoint: {err}")))?
409 .set_pkce_verifier(openidconnect::PkceCodeVerifier::new(
410 pending.pkce_verifier.clone(),
411 ))
412 .request_async(&self.http)
413 .await
414 .map_err(|err| Error::Oidc(format!("exchanging the code failed: {err}")))?;
415 let id_token = openidconnect::TokenResponse::id_token(&response)
416 .ok_or_else(|| Error::Oidc("the provider returned no ID token".into()))?;
417 let verifier = client.id_token_verifier().set_allowed_algs(allowed_algs());
418 id_token
419 .claims(&verifier, &Nonce::new(pending.nonce.clone()))
420 .map_err(|err| Error::Oidc(format!("invalid ID token: {err}")))?;
421 Ok(id_token.to_string())
422 }
423}