use std::time::Duration;
use base64::Engine as _;
use base64::engine::general_purpose;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use crate::{Claims, Error, Jwt, Permission, RedapClaims};
const AUDIENCE: &str = "redap";
#[derive(Debug, Clone)]
pub struct RedapProvider {
secret_key: SecretKey,
#[cfg(feature = "oauth")]
oauth: Option<RerunCloudProvider>,
}
#[cfg(feature = "oauth")]
#[derive(Debug, Clone)]
struct RerunCloudProvider {
keys: jsonwebtoken::jwk::JwkSet,
org_id: String,
}
#[derive(Clone, PartialEq, Eq)]
pub struct SecretKey(Vec<u8>);
impl SecretKey {
#[inline]
pub fn reveal(&self) -> &[u8] {
&self.0
}
pub fn generate(rng: impl rand::Rng) -> Self {
let secret_key = generate_secret_key(rng, 32);
re_log::debug_assert_eq!(
secret_key.len() * size_of::<u8>() * 8,
256,
"The resulting secret should be 256 bits."
);
Self(secret_key)
}
pub fn from_base64(base64: impl AsRef<str>) -> Result<Self, Error> {
Ok(Self(general_purpose::STANDARD.decode(base64.as_ref())?))
}
pub fn to_base64(&self) -> String {
general_purpose::STANDARD.encode(&self.0)
}
}
impl std::fmt::Debug for SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("********")
}
}
#[derive(Debug, Clone)]
pub struct VerificationOptions {
leeway: Option<Duration>,
}
impl VerificationOptions {
#[inline]
pub fn with_leeway(mut self, leeway: Option<Duration>) -> Self {
self.leeway = leeway;
self
}
#[inline]
pub fn without_leeway(mut self) -> Self {
self.leeway = None;
self
}
}
impl Default for VerificationOptions {
fn default() -> Self {
Self {
leeway: Some(Duration::from_secs(5 * 60)),
}
}
}
#[derive(Clone, Copy)]
enum KeyProvider {
#[cfg(feature = "oauth")]
RerunCloud,
Redap,
}
impl VerificationOptions {
fn for_provider(self, provider: KeyProvider) -> Validation {
match provider {
#[cfg(feature = "oauth")]
KeyProvider::RerunCloud => {
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[&*crate::oauth::OAUTH_ISSUER_URL]);
validation.required_spec_claims.extend(
crate::oauth::RerunCloudClaims::REQUIRED
.iter()
.copied()
.map(String::from),
);
validation.validate_exp = true;
validation.leeway = self.leeway.map_or(0, |leeway| leeway.as_secs());
validation
}
KeyProvider::Redap => {
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&[AUDIENCE.to_owned()]);
validation.set_required_spec_claims(&["exp", "sub", "aud", "iss"]);
validation.leeway = self.leeway.map_or(0, |leeway| leeway.as_secs());
validation
}
}
}
}
fn generate_secret_key(mut rng: impl rand::Rng, length: usize) -> Vec<u8> {
(0..length).map(|_| rng.random::<u8>()).collect()
}
impl RedapProvider {
pub fn from_secret_key(secret_key: SecretKey) -> Self {
crate::crypto_provider::install();
Self {
secret_key,
#[cfg(feature = "oauth")]
oauth: None,
}
}
pub fn from_secret_key_base64(secret_key: &str) -> Result<Self, Error> {
crate::crypto_provider::install();
Ok(Self {
secret_key: SecretKey::from_base64(secret_key)?,
#[cfg(feature = "oauth")]
oauth: None,
})
}
#[cfg(feature = "oauth")]
pub async fn with_rerun_cloud_provider(self, org_id: impl Into<String>) -> Result<Self, Error> {
use crate::oauth::api;
let keys = api::jwks().await.map_err(|err| {
re_log::debug!("failed to fetch external keys: {err}");
Error::JwksFetch(err)
})?;
let org_id = org_id.into();
let provider = RerunCloudProvider { keys, org_id };
Ok(Self {
secret_key: self.secret_key,
oauth: Some(provider),
})
}
pub fn token(
&self,
duration: Duration,
issuer: impl Into<String>,
subject: impl Into<String>,
permission: Permission,
allowed_host: Option<&str>,
) -> Result<Jwt, Error> {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?;
let allowed_hosts = allowed_host.map(|h| vec![h.to_owned()]).unwrap_or_default();
let claims = Claims::Redap(RedapClaims {
iss: issuer.into(),
sub: subject.into(),
aud: vec![AUDIENCE.to_owned()],
exp: (now + duration).as_secs(),
iat: now.as_secs(),
permissions: vec![permission],
allowed_hosts,
});
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.secret_key.reveal()),
)?;
Ok(Jwt(token))
}
pub fn verify(&self, token: &Jwt, options: VerificationOptions) -> Result<Claims, Error> {
#[cfg(feature = "oauth")]
let (key, validation) = if let Some(kid) = jsonwebtoken::decode_header(token.as_str())?.kid
{
let Some(provider) = &self.oauth else {
return Err(Error::NoExternalProvider);
};
let Some(key) = provider.keys.find(&kid) else {
re_log::debug!("no key with id {kid} found");
return Err(Error::InvalidToken);
};
let key = DecodingKey::from_jwk(key)?;
let validation = options.for_provider(KeyProvider::RerunCloud);
(key, validation)
} else {
let key = DecodingKey::from_secret(self.secret_key.reveal());
let validation = options.for_provider(KeyProvider::Redap);
(key, validation)
};
#[cfg(not(feature = "oauth"))]
let (key, validation) = {
let key = DecodingKey::from_secret(self.secret_key.reveal());
let validation = options.for_provider(KeyProvider::Redap);
(key, validation)
};
let token_data = decode::<Claims>(&token.0, &key, &validation)?;
match &token_data.claims {
#[cfg(feature = "oauth")]
Claims::RerunCloud(claims) => {
let provider = self
.oauth
.as_ref()
.expect("bug: verified external key without external provider configured");
if claims.org_id != provider.org_id {
re_log::debug!(
"verification failed: organization ID was not {}",
provider.org_id
);
return Err(Error::InvalidToken);
}
}
Claims::Redap(_) => {
}
}
Ok(token_data.claims)
}
}