Separate bearer ids from private tokens
This commit is contained in:
parent
3382899628
commit
460acab67b
9 changed files with 198 additions and 62 deletions
|
|
@ -23,6 +23,8 @@ pub struct ResourceKeyEnvelope {
|
|||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BearerAccess {
|
||||
pub secret: SecretId,
|
||||
pub token: Option<SecretId>,
|
||||
pub token_hash: Option<String>,
|
||||
pub resource: ResourceId,
|
||||
pub capabilities: Vec<Capability>,
|
||||
pub expires_at: Option<UnixMillis>,
|
||||
|
|
@ -55,6 +57,8 @@ impl BearerAccess {
|
|||
) -> Self {
|
||||
Self {
|
||||
secret,
|
||||
token: None,
|
||||
token_hash: None,
|
||||
resource,
|
||||
capabilities,
|
||||
expires_at: None,
|
||||
|
|
@ -93,6 +97,17 @@ pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), S
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bearer_token_hash(token: &SecretId) -> String {
|
||||
blake3::hash(token.as_str().as_bytes()).to_hex().to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bearer_id_for_token(token: &SecretId) -> SecretId {
|
||||
let hash = bearer_token_hash(token);
|
||||
SecretId::new(format!("bearer:{}", &hash[..32]))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bearer_response(
|
||||
secret: &SecretId,
|
||||
|
|
@ -119,6 +134,34 @@ pub fn bearer_response(
|
|||
.to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bearer_response_with_token_hash(
|
||||
token_hash: &str,
|
||||
resource: &ResourceId,
|
||||
capabilities: &[Capability],
|
||||
nonce: &str,
|
||||
) -> Option<String> {
|
||||
let hash_bytes = hex_to_32_bytes(token_hash)?;
|
||||
let mut capability_strings = capabilities
|
||||
.iter()
|
||||
.map(|capability| capability.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
capability_strings.sort_unstable();
|
||||
let mut message = String::new();
|
||||
message.push_str(resource.as_str());
|
||||
message.push('\0');
|
||||
message.push_str(nonce);
|
||||
for capability in capability_strings {
|
||||
message.push('\0');
|
||||
message.push_str(capability);
|
||||
}
|
||||
Some(
|
||||
blake3::keyed_hash(&hash_bytes, message.as_bytes())
|
||||
.to_hex()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn verify_bearer_response(
|
||||
secret: &SecretId,
|
||||
|
|
@ -130,6 +173,29 @@ pub fn verify_bearer_response(
|
|||
bearer_response(secret, resource, capabilities, nonce) == response
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn verify_bearer_response_with_token_hash(
|
||||
token_hash: &str,
|
||||
resource: &ResourceId,
|
||||
capabilities: &[Capability],
|
||||
nonce: &str,
|
||||
response: &str,
|
||||
) -> bool {
|
||||
bearer_response_with_token_hash(token_hash, resource, capabilities, nonce)
|
||||
.is_some_and(|expected| expected == response)
|
||||
}
|
||||
|
||||
fn hex_to_32_bytes(hex: &str) -> Option<[u8; 32]> {
|
||||
if hex.len() != 64 {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = [0_u8; 32];
|
||||
for index in 0..32 {
|
||||
bytes[index] = u8::from_str_radix(&hex[index * 2..index * 2 + 2], 16).ok()?;
|
||||
}
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -153,7 +219,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn bearer_response_is_resource_scoped_and_capability_scoped() {
|
||||
let secret = SecretId::new("bearer:test");
|
||||
let secret = SecretId::new("gbt_test");
|
||||
let resource = ResourceId::new("resource:kv:prefs");
|
||||
let capabilities = vec![Capability::new("kv.read"), Capability::new("kv.write")];
|
||||
|
||||
|
|
@ -181,4 +247,31 @@ mod tests {
|
|||
&response
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearer_id_is_separate_from_private_token() {
|
||||
let token = SecretId::new("gbt_private_token");
|
||||
let bearer_id = bearer_id_for_token(&token);
|
||||
let token_hash = bearer_token_hash(&token);
|
||||
let resource = ResourceId::new("resource:cas:local");
|
||||
let capabilities = vec![Capability::new("cas.fetch")];
|
||||
let response = bearer_response(&token, &resource, &capabilities, "nonce");
|
||||
|
||||
assert_ne!(bearer_id, token);
|
||||
assert!(bearer_id.as_str().starts_with("bearer:"));
|
||||
assert!(verify_bearer_response_with_token_hash(
|
||||
&token_hash,
|
||||
&resource,
|
||||
&capabilities,
|
||||
"nonce",
|
||||
&response
|
||||
));
|
||||
assert!(!verify_bearer_response_with_token_hash(
|
||||
&token_hash,
|
||||
&resource,
|
||||
&[Capability::new("cas.pin")],
|
||||
"nonce",
|
||||
&response
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue