use std::time::Duration;
use base64::Engine as _;
use base64::engine::general_purpose;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use crate::{Error, Jwt, Permission};
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, serde::Serialize, serde::Deserialize)]
pub struct RedapClaims {
pub iss: String,
pub sub: String,
#[serde(
deserialize_with = "deser_string_or_vec",
serialize_with = "ser_string_or_vec"
)]
pub aud: Vec<String>,
pub exp: u64,
pub iat: u64,
#[serde(default)]
pub permissions: Vec<Permission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_hosts: Vec<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Claims {
#[cfg(feature = "oauth")]
RerunCloud(crate::oauth::RerunCloudClaims),
Redap(RedapClaims),
}
impl Claims {
pub fn sub(&self) -> &str {
match self {
#[cfg(feature = "oauth")]
Self::RerunCloud(claims) => claims.email.as_deref().unwrap_or(claims.sub.as_str()),
Self::Redap(claims) => claims.sub.as_str(),
}
}
pub fn iss(&self) -> &str {
match self {
#[cfg(feature = "oauth")]
Self::RerunCloud(claims) => claims.iss.as_str(),
Self::Redap(claims) => claims.iss.as_str(),
}
}
pub fn permissions(&self) -> &[Permission] {
match self {
#[cfg(feature = "oauth")]
Self::RerunCloud(claims) => &claims.permissions[..],
Self::Redap(claims) => &claims.permissions[..],
}
}
pub fn has_read_permission(&self) -> bool {
self.permissions().iter().any(|p| p == &Permission::Read)
}
pub fn has_write_permission(&self) -> bool {
self.permissions()
.iter()
.any(|p| p == &Permission::ReadWrite)
}
}
#[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)
}
}
fn deser_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum StringOrVec {
One(String),
Many(Vec<String>),
}
use serde::Deserialize as _;
match StringOrVec::deserialize(deserializer)? {
StringOrVec::One(s) => Ok(vec![s]),
StringOrVec::Many(v) => Ok(v),
}
}
fn ser_string_or_vec<S>(value: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::Serialize as _;
if value.len() == 1 {
serializer.serialize_str(&value[0])
} else {
value.serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aud_deserialize_single_string() {
let json = r#"{
"iss": "test",
"sub": "user123",
"aud": "redap",
"exp": 1234567890,
"iat": 1234567890
}"#;
let claims: RedapClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.aud, vec!["redap"]);
assert!(claims.allowed_hosts.is_empty());
}
#[test]
fn test_aud_deserialize_array() {
let json = r#"{
"iss": "test",
"sub": "user123",
"aud": ["redap", "other-service"],
"exp": 1234567890,
"iat": 1234567890
}"#;
let claims: RedapClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.aud, vec!["redap", "other-service"]);
}
#[test]
fn test_aud_deserialize_empty_array() {
let json = r#"{
"iss": "test",
"sub": "user123",
"aud": [],
"exp": 1234567890,
"iat": 1234567890
}"#;
let claims: RedapClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.aud, Vec::<String>::new());
}
#[test]
fn test_allowed_hosts_deserialize() {
let json = r#"{
"iss": "test",
"sub": "user123",
"aud": "redap",
"exp": 1234567890,
"iat": 1234567890,
"allowed_hosts": ["api.acme.cloud.rerun.io"]
}"#;
let claims: RedapClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.aud, vec!["redap"]);
assert_eq!(claims.allowed_hosts, vec!["api.acme.cloud.rerun.io"]);
}
#[test]
fn test_aud_serialize_single() {
let claims = RedapClaims {
iss: "test".to_owned(),
sub: "user123".to_owned(),
aud: vec!["redap".to_owned()],
exp: 1234567890,
iat: 1234567890,
permissions: vec![],
allowed_hosts: vec![],
};
let json = serde_json::to_value(&claims).unwrap();
assert_eq!(json["aud"], serde_json::json!("redap"));
assert!(json.get("allowed_hosts").is_none());
}
#[test]
fn test_aud_serialize_multiple() {
let claims = RedapClaims {
iss: "test".to_owned(),
sub: "user123".to_owned(),
aud: vec!["redap".to_owned(), "other".to_owned()],
exp: 1234567890,
iat: 1234567890,
permissions: vec![],
allowed_hosts: vec![],
};
let json = serde_json::to_value(&claims).unwrap();
assert_eq!(json["aud"], serde_json::json!(["redap", "other"]));
}
#[test]
fn test_allowed_hosts_serialize() {
let claims = RedapClaims {
iss: "test".to_owned(),
sub: "user123".to_owned(),
aud: vec!["redap".to_owned()],
exp: 1234567890,
iat: 1234567890,
permissions: vec![],
allowed_hosts: vec!["api.acme.cloud.rerun.io".to_owned()],
};
let json = serde_json::to_value(&claims).unwrap();
assert_eq!(
json["allowed_hosts"],
serde_json::json!(["api.acme.cloud.rerun.io"])
);
}
}