context: Do not cache OIDC client
It turns out, we do NOT want to keep one single, global OIDC client data structure. There are two major problems with this: 1. If the OIDC IdP happens to be unavailable when the process starts, Rocket will fail to ignite and the process will exit. This is unnecessary, since the only functionality that will be unavailable without the IdP is new logins; existing sessions/tokens will still be valid. 2. Identity providers can change keys, URLs, etc. at any time. If we cache everything and never look it up again, all future login attempts will fail until the server is restarted. The official recommendation for caching OIDC IdP configuration and keys is to use native HTTP cache control. Unfortunately, most IdPs explicitly disable caching of their HTTP responses.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/Rocket.toml
|
/Rocket.toml
|
||||||
/jwt.secret
|
/jwt.secret
|
||||||
/meilisearch.token
|
/meilisearch.token
|
||||||
|
/oidc.secret
|
||||||
|
|||||||
49
src/auth.rs
49
src/auth.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use std::path::Path;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
@@ -177,18 +178,15 @@ impl From<LoginError> for LoginFailed {
|
|||||||
|
|
||||||
/// Create an openidconnect Client instance from the given Config
|
/// Create an openidconnect Client instance from the given Config
|
||||||
pub async fn get_oidc_client(
|
pub async fn get_oidc_client(
|
||||||
config: &Config,
|
discovery_url: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
callback_url: String,
|
||||||
|
http_client: &reqwest::Client,
|
||||||
) -> Result<OidcClient, OidcError> {
|
) -> Result<OidcClient, OidcError> {
|
||||||
let http_client = reqwest::ClientBuilder::new()
|
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let oidc_url = config.oidc.discovery_url.clone();
|
|
||||||
let client_id = config.oidc.client_id.clone();
|
|
||||||
let client_secret = config.oidc.client_secret.clone();
|
|
||||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||||
IssuerUrl::new(oidc_url)?,
|
IssuerUrl::new(discovery_url)?,
|
||||||
&http_client,
|
http_client,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(CoreClient::from_provider_metadata(
|
Ok(CoreClient::from_provider_metadata(
|
||||||
@@ -196,13 +194,13 @@ pub async fn get_oidc_client(
|
|||||||
ClientId::new(client_id),
|
ClientId::new(client_id),
|
||||||
client_secret.map(ClientSecret::new),
|
client_secret.map(ClientSecret::new),
|
||||||
)
|
)
|
||||||
.set_redirect_uri(RedirectUrl::new(config.oidc.callback_url.clone())?))
|
.set_redirect_uri(RedirectUrl::new(callback_url)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the JWT secret from the path specified in the configuration
|
/// Load a secret from the specified path
|
||||||
pub fn load_secret(config: &Config) -> Result<Vec<u8>, std::io::Error> {
|
pub fn load_secret<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, std::io::Error> {
|
||||||
let mut secret = vec![];
|
let mut secret = vec![];
|
||||||
let mut f = std::fs::File::open(&config.auth.jwt_secret)?;
|
let mut f = std::fs::File::open(path)?;
|
||||||
f.read_to_end(&mut secret)?;
|
f.read_to_end(&mut secret)?;
|
||||||
Ok(secret)
|
Ok(secret)
|
||||||
}
|
}
|
||||||
@@ -263,15 +261,12 @@ async fn exchange_code(
|
|||||||
) -> Result<CoreIdTokenClaims, LoginError> {
|
) -> Result<CoreIdTokenClaims, LoginError> {
|
||||||
let pkce_verifier = PkceCodeVerifier::new(state.pkce);
|
let pkce_verifier = PkceCodeVerifier::new(state.pkce);
|
||||||
debug!("Exchanging authorization code for access token");
|
debug!("Exchanging authorization code for access token");
|
||||||
let http_client = reqwest::ClientBuilder::new()
|
let http_client = &ctx.oidc_http_client;
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
let oidc = ctx.oidc().await?;
|
||||||
.build()
|
let token_response = oidc
|
||||||
.unwrap();
|
|
||||||
let token_response = ctx
|
|
||||||
.oidc
|
|
||||||
.exchange_code(AuthorizationCode::new(code))?
|
.exchange_code(AuthorizationCode::new(code))?
|
||||||
.set_pkce_verifier(pkce_verifier)
|
.set_pkce_verifier(pkce_verifier)
|
||||||
.request_async(&http_client)
|
.request_async(http_client)
|
||||||
.await
|
.await
|
||||||
.map_err(LoginError::TokenRequestError)?;
|
.map_err(LoginError::TokenRequestError)?;
|
||||||
debug!(
|
debug!(
|
||||||
@@ -284,9 +279,9 @@ async fn exchange_code(
|
|||||||
let id_token = token_response
|
let id_token = token_response
|
||||||
.id_token()
|
.id_token()
|
||||||
.ok_or(LoginError::MissingIdToken)?;
|
.ok_or(LoginError::MissingIdToken)?;
|
||||||
let id_token_verifier = ctx.oidc.id_token_verifier();
|
let id_token_verifier = oidc.id_token_verifier();
|
||||||
let claims = id_token
|
let claims = id_token
|
||||||
.claims(&ctx.oidc.id_token_verifier(), &Nonce::new(state.nonce))?;
|
.claims(&oidc.id_token_verifier(), &Nonce::new(state.nonce))?;
|
||||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||||
token_response.access_token(),
|
token_response.access_token(),
|
||||||
@@ -306,13 +301,13 @@ pub async fn oidc_login(
|
|||||||
ctx: &State<Context>,
|
ctx: &State<Context>,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
config: &RocketConfig,
|
config: &RocketConfig,
|
||||||
) -> Redirect {
|
) -> Result<Redirect, LoginFailed> {
|
||||||
let (pkce_challenge, pkce_verifier) =
|
let (pkce_challenge, pkce_verifier) =
|
||||||
PkceCodeChallenge::new_random_sha256();
|
PkceCodeChallenge::new_random_sha256();
|
||||||
trace!("PKCE: {:?} {:?}", pkce_challenge, pkce_verifier);
|
trace!("PKCE: {:?} {:?}", pkce_challenge, pkce_verifier);
|
||||||
|
|
||||||
let (auth_url, csrf_token, nonce) = ctx
|
let oidc = ctx.oidc().await.map_err(LoginError::from)?;
|
||||||
.oidc
|
let (auth_url, csrf_token, nonce) = oidc
|
||||||
.authorize_url(
|
.authorize_url(
|
||||||
CoreAuthenticationFlow::AuthorizationCode,
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
CsrfToken::new_random,
|
CsrfToken::new_random,
|
||||||
@@ -355,7 +350,7 @@ pub async fn oidc_login(
|
|||||||
error!("Failed to serialize OIDC client state: {}", e);
|
error!("Failed to serialize OIDC client state: {}", e);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Redirect::to(auth_url.to_string())
|
Ok(Redirect::to(auth_url.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle OpenID Connect authorization callback
|
/// Handle OpenID Connect authorization callback
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub struct AuthConfig {
|
|||||||
pub login_ttl: u64,
|
pub login_ttl: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct OidcConfig {
|
pub struct OidcConfig {
|
||||||
pub discovery_url: String,
|
pub discovery_url: String,
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
|
|||||||
12
src/error.rs
12
src/error.rs
@@ -51,16 +51,18 @@ pub enum LoginError {
|
|||||||
InvalidAccessToken,
|
InvalidAccessToken,
|
||||||
#[error("JWT serialization error: {0}")]
|
#[error("JWT serialization error: {0}")]
|
||||||
JwtError(#[from] jsonwebtoken::errors::Error),
|
JwtError(#[from] jsonwebtoken::errors::Error),
|
||||||
|
#[error("Invalid OIDC configuration: {0}")]
|
||||||
|
Oidc(#[from] OidcError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("Meilisearch error: {0}")]
|
#[error("Meilisearch error: {0}")]
|
||||||
Meilisearch(#[from] crate::meilisearch::Error),
|
Meilisearch(#[from] crate::meilisearch::Error),
|
||||||
|
|
||||||
#[error("Failed to load JWT secret: {0}")]
|
#[error("Failed to load JWT secret: {0}")]
|
||||||
LoadSecret(std::io::Error),
|
LoadJwtSecret(std::io::Error),
|
||||||
|
#[error("Failed to load OIDC client secret: {0}")]
|
||||||
#[error("Invalid OIDC configuration: {0}")]
|
LoadOidcSecret(std::io::Error),
|
||||||
Oidc(#[from] OidcError),
|
#[error("Failed to initialize HTTP client: {0}")]
|
||||||
|
ReqwestError(#[from] reqwest::Error)
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/lib.rs
33
src/lib.rs
@@ -15,21 +15,46 @@ pub use error::InitError;
|
|||||||
pub struct Context {
|
pub struct Context {
|
||||||
client: MeilisearchClient,
|
client: MeilisearchClient,
|
||||||
jwt_secret: Vec<u8>,
|
jwt_secret: Vec<u8>,
|
||||||
oidc: auth::OidcClient,
|
oidc: config::OidcConfig,
|
||||||
|
oidc_client_secret: Option<String>,
|
||||||
|
oidc_http_client: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub async fn init(config: &Config) -> Result<Self, InitError> {
|
pub async fn init(config: &Config) -> Result<Self, InitError> {
|
||||||
let client = MeilisearchClient::try_from(config)?;
|
let client = MeilisearchClient::try_from(config)?;
|
||||||
let jwt_secret =
|
let jwt_secret = auth::load_secret(&config.auth.jwt_secret)
|
||||||
auth::load_secret(config).map_err(InitError::LoadSecret)?;
|
.map_err(InitError::LoadJwtSecret)?;
|
||||||
let oidc = auth::get_oidc_client(config).await?;
|
let oidc = config.oidc.clone();
|
||||||
|
let oidc_client_secret = match &config.oidc.client_secret {
|
||||||
|
Some(p) => match auth::load_secret(p) {
|
||||||
|
Ok(s) => Some(String::from_utf8_lossy(&s).trim().to_string()),
|
||||||
|
Err(e) => return Err(InitError::LoadOidcSecret(e)),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let oidc_http_client = reqwest::ClientBuilder::new()
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
jwt_secret,
|
jwt_secret,
|
||||||
oidc,
|
oidc,
|
||||||
|
oidc_client_secret,
|
||||||
|
oidc_http_client,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn oidc(&self) -> Result<auth::OidcClient, error::OidcError> {
|
||||||
|
auth::get_oidc_client(
|
||||||
|
self.oidc.discovery_url.clone(),
|
||||||
|
self.oidc.client_id.clone(),
|
||||||
|
self.oidc_client_secret.clone(),
|
||||||
|
self.oidc.callback_url.clone(),
|
||||||
|
&self.oidc_http_client,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the application context
|
/// Initialize the application context
|
||||||
|
|||||||
Reference in New Issue
Block a user