1
0
Fork 0
mirror of https://github.com/zhaofengli/attic.git synced 2025-03-05 08:17:05 +00:00

server: support HS256, RS256 JWT secrets

This commit is contained in:
Cole Helbling 2023-11-09 13:57:12 -08:00
parent dcd7d7fe87
commit 427ae4550b
9 changed files with 170 additions and 32 deletions

1
Cargo.lock generated
View file

@ -301,6 +301,7 @@ dependencies = [
"humantime",
"humantime-serde",
"itoa",
"jsonwebtoken",
"maybe-owned",
"rand",
"regex",

View file

@ -60,6 +60,7 @@ tracing-subscriber = { version = "0.3.17", features = [ "json" ] }
uuid = { version = "1.3.3", features = ["v4"] }
console-subscriber = "0.1.9"
xdg = "2.5.0"
jsonwebtoken = "9.1.0"
[dependencies.async-compression]
version = "0.4.0"

View file

@ -1,10 +1,12 @@
//! HTTP middlewares for access control.
use axum::{http::Request, middleware::Next, response::Response};
use jsonwebtoken::Algorithm;
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
use crate::access::{CachePermission, Token};
use crate::config::JWTSigningConfig;
use crate::database::{entity::cache::CacheModel, AtticDatabase};
use crate::error::ServerResult;
use crate::{RequestState, State};
@ -101,9 +103,19 @@ pub async fn apply_auth<B>(req: Request<B>, next: Next<B>) -> Response {
.and_then(parse_authorization_header)
.and_then(|jwt| {
let state = req.extensions().get::<State>().unwrap();
let (algorithm, decoding_key) = match &state.config.jwt.signing_config {
JWTSigningConfig::HS256SignAndVerify { decoding_key, .. } => {
(Algorithm::HS256, decoding_key)
}
JWTSigningConfig::RS256SignAndVerify { decoding_key, .. } => {
(Algorithm::RS256, decoding_key)
}
};
let res_token = Token::from_jwt(
&jwt,
&state.config.jwt.token_rs256_secret.1,
algorithm,
decoding_key,
&state.config.jwt.token_bound_issuer,
&state.config.jwt.token_bound_audiences,
);

View file

@ -2,11 +2,12 @@ use anyhow::{anyhow, Result};
use chrono::{Duration as ChronoDuration, Utc};
use clap::Parser;
use humantime::Duration;
use jsonwebtoken::Algorithm;
use crate::Opts;
use attic::cache::CacheNamePattern;
use attic_server::access::Token;
use attic_server::config::Config;
use attic_server::config::{Config, JWTSigningConfig};
/// Generate a new token.
///
@ -115,8 +116,18 @@ pub async fn run(config: Config, opts: Opts) -> Result<()> {
if sub.dump_claims {
println!("{}", serde_json::to_string(token.opaque_claims())?);
} else {
let (algorithm, encoding_key) = match &config.jwt.signing_config {
JWTSigningConfig::HS256SignAndVerify { encoding_key, .. } => {
(Algorithm::HS256, encoding_key)
}
JWTSigningConfig::RS256SignAndVerify { encoding_key, .. } => {
(Algorithm::RS256, encoding_key)
}
};
let encoded_token = token.encode(
&config.jwt.token_rs256_secret.0,
algorithm,
&encoding_key,
&config.jwt.token_bound_issuer,
&config.jwt.token_bound_audiences,
)?;

View file

@ -131,14 +131,7 @@ interval = "12 hours"
[jwt]
# WARNING: Changing _anything_ in this section will break any existing
# tokens. If you need to regenerate them, ensure that you use the the
# RS256 secret configured below and include the `iss` and `aud` claims.
# JWT signing token
#
# Set this to the base64-encoded private half of an RSA PEM PKCS1.
# You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64`
# environment variable.
token-rs256-secret-base64 = "%token_rs256_secret_base64%"
# correct secret and include the `iss` and `aud` claims.
# JWT `iss` claim
#
@ -153,3 +146,18 @@ token-rs256-secret-base64 = "%token_rs256_secret_base64%"
# If this is set, all received JWTs will validate that the `aud` claim
# contains at least one of these values.
#token-bound-audiences = ["some-audience1", "some-audience2"]
[jwt.signing]
# JWT RS256 secret key
#
# Set this to the base64-encoded private half of an RSA PEM PKCS1 key.
# You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64`
# environment variable.
token-rs256-secret-base64 = "%token_rs256_secret_base64%"
# JWT HS256 secret key
#
# Set this to the base64-encoded HMAC secret key.
# You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64`
# environment variable.
#token-hs256-secret-base64 = ""

View file

@ -12,7 +12,9 @@ use derivative::Derivative;
use serde::{de, Deserialize};
use xdg::BaseDirectories;
use crate::access::{decode_token_rs256_secret, DecodingKey, EncodingKey};
use crate::access::{
decode_token_hs256_secret, decode_token_rs256_secret, DecodingKey, EncodingKey,
};
use crate::narinfo::Compression as NixCompression;
use crate::storage::{LocalStorageConfig, S3StorageConfig};
@ -26,6 +28,10 @@ const XDG_PREFIX: &str = "attic";
/// This is useful for deploying to certain application platforms like Fly.io
const ENV_CONFIG_BASE64: &str = "ATTIC_SERVER_CONFIG_BASE64";
/// Environment variable storing the base64-encoded HMAC secret (used for signing and verifying
/// received JWTs).
const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64";
/// Environment variable storing the base64-encoded RSA PEM PKCS1 private key (used for signing and
/// verifying received JWTs).
const ENV_TOKEN_RS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64";
@ -117,15 +123,6 @@ pub struct Config {
#[derive(Clone, Derivative, Deserialize)]
#[derivative(Debug)]
pub struct JWTConfig {
/// JSON Web Token RSA secret.
///
/// Set this to the base64-encoded RSA PEM PKCS1 private key.
#[serde(rename = "token-rs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_secret_base64")]
#[serde(default = "load_token_rs256_secret_from_env")]
#[derivative(Debug = "ignore")]
pub token_rs256_secret: (EncodingKey, DecodingKey),
/// The `iss` claim of the JWT.
///
/// If specified, received JWTs must have this claim, and its value must match this
@ -141,6 +138,35 @@ pub struct JWTConfig {
#[serde(rename = "token-bound-audiences")]
#[serde(default = "Default::default")]
pub token_bound_audiences: Option<Vec<String>>,
#[serde(rename = "signing")]
#[serde(default = "load_jwt_signing_config_from_env")]
#[derivative(Debug = "ignore")]
pub signing_config: JWTSigningConfig,
}
#[derive(Clone, Deserialize)]
pub enum JWTSigningConfig {
/// JSON Web Token HMAC secret.
///
/// Set this to the base64-encoded HMAC secret to use for signing and verifying JWTs.
#[serde(rename = "token-hs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_hs256_secret_base64")]
HS256SignAndVerify {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
},
/// JSON Web Token RSA secret.
///
/// Set this to the base64-encoded RSA PEM PKCS1 private key to use for signing and verifying
/// JWTs.
#[serde(rename = "token-rs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_secret_base64")]
RS256SignAndVerify {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
},
}
/// Database connection configuration.
@ -265,13 +291,60 @@ pub struct GarbageCollectionConfig {
pub default_retention_period: Duration,
}
fn load_token_rs256_secret_from_env() -> (EncodingKey, DecodingKey) {
let s = env::var(ENV_TOKEN_RS256_SECRET_BASE64).expect(&format!(
"The RS256 secret must be specified in either jwt.token-rs256-secret-base64 \
or the {ENV_TOKEN_RS256_SECRET_BASE64} environment."
));
fn load_jwt_signing_config_from_env() -> JWTSigningConfig {
let config = if let Some(config) = load_token_rs256_secret_from_env() {
config
} else if let Some(config) = load_token_hs256_secret_from_env() {
config
} else {
panic!(
"\n\
You must configure JWT signing and verification inside the [jwt.signing] block with \
one of the following settings:\n\
\n\
* token-rs256-secret-base64\n\
* token-hs256-secret-base64\n\
\n\
or by setting one of the following environment variables:\n\
\n\
* {ENV_TOKEN_RS256_SECRET_BASE64}\n\
* {ENV_TOKEN_HS256_SECRET_BASE64}\n\
\n\
An RS256 secret will be used for both signing new JWTs and verifying received JWTs \
with the provided RSA (asymmetric) PEM PKCS1 private key.\n\
An HS256 secret will be used for both signing new JWTs and verifying received JWTs \
with the provided HMAC (symmetric) secret.\n\
"
)
};
decode_token_rs256_secret(&s).expect("Failed to load as decoding key")
config
}
fn load_token_hs256_secret_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64).ok()?;
decode_token_hs256_secret(&s)
.ok()
.map(
|(encoding_key, decoding_key)| JWTSigningConfig::HS256SignAndVerify {
encoding_key,
decoding_key,
},
)
}
fn load_token_rs256_secret_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_RS256_SECRET_BASE64).ok()?;
decode_token_rs256_secret(&s)
.ok()
.map(
|(encoding_key, decoding_key)| JWTSigningConfig::RS256SignAndVerify {
encoding_key,
decoding_key,
},
)
}
fn load_database_url_from_env() -> String {
@ -325,6 +398,20 @@ impl Default for GarbageCollectionConfig {
}
}
fn deserialize_token_hs256_secret_base64<'de, D>(
deserializer: D,
) -> Result<(EncodingKey, DecodingKey), D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
let s = String::deserialize(deserializer)?;
let key = decode_token_hs256_secret(&s).map_err(Error::custom)?;
Ok(key)
}
fn deserialize_token_rs256_secret_base64<'de, D>(
deserializer: D,
) -> Result<(EncodingKey, DecodingKey), D::Error>

View file

@ -77,7 +77,7 @@ pub async fn run_oobe() -> Result<()> {
perm.destroy_cache = true;
let key = decode_token_rs256_secret(&rs256_secret_base64).unwrap();
token.encode(&key.0, &None, &None)?
token.encode(jsonwebtoken::Algorithm::RS256, &key.0, &None, &None)?
};
eprintln!();

View file

@ -1,7 +1,7 @@
//! Access control.
//!
//! Access control in Attic is simple and stateless [0] - The server validates
//! the JWT against a RS256 key and allows access based on the `https://jwt.attic.rs/v1`
//! the JWT against the configured key and allows access based on the `https://jwt.attic.rs/v1`
//! claim.
//!
//! One primary goal of the Attic Server is easy scalability. It's designed
@ -287,13 +287,14 @@ impl Token {
/// Verifies and decodes a token.
pub fn from_jwt(
token: &str,
key_algorithm: Algorithm,
key: &jsonwebtoken::DecodingKey,
maybe_bound_issuer: &Option<String>,
maybe_bound_audiences: &Option<Vec<String>>,
) -> Result<Self> {
let mut required_spec_claims = vec!["exp", "nbf", "sub"];
let mut validation = Validation::new(Algorithm::RS256);
let mut validation = Validation::new(key_algorithm);
validation.validate_nbf = true;
if let Some(bound_issuer) = maybe_bound_issuer {
@ -337,11 +338,12 @@ impl Token {
/// Encodes the token.
pub fn encode(
&self,
key_algorithm: Algorithm,
key: &jsonwebtoken::EncodingKey,
maybe_bound_issuer: &Option<String>,
maybe_bound_audiences: &Option<Vec<String>>,
) -> Result<String> {
let header = jsonwebtoken::Header::new(Algorithm::RS256);
let header = jsonwebtoken::Header::new(key_algorithm);
let mut claims = self.0.clone();
claims.issuer = maybe_bound_issuer.to_owned();
@ -452,6 +454,15 @@ impl CachePermission {
impl StdError for Error {}
pub fn decode_token_hs256_secret(s: &str) -> Result<(EncodingKey, DecodingKey)> {
let secret = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let encoding_key = EncodingKey::from_secret(&secret);
let decoding_key = DecodingKey::from_secret(&secret);
Ok((encoding_key, decoding_key))
}
pub fn decode_token_rs256_secret(s: &str) -> Result<(EncodingKey, DecodingKey)> {
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;

View file

@ -35,7 +35,14 @@ fn test_basic() {
// TOKEN=$(jq -c < json | jwt encode --alg RS256 --secret @./rs256 -)
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJjYWNoZS1ybyI6eyJyIjoxfSwiY2FjaGUtcnciOnsiciI6MSwidyI6MX0sInRlYW0tKiI6eyJjYyI6MSwiciI6MSwidyI6MX19fSwiaWF0IjoxNjk5NzM0NTU3LCJuYmYiOjAsInN1YiI6Im1lb3cifQ.k1TCqAg5_yaBQByKnYn5zSvMsYi8XrHe1h8T2hijZiP1SsYYnKphKKm0e61lmr3tSM-3dtRRCNGB7elhetpuz2jz8fWyBmpjO-yIX2uB787iRKVjaVCEKSPjcKO9lGp9LlxKdNH0SLRmdwkJGQUHbzN6QurfiV4C54cPxC_43EamkOqFUFmmwohi_r76RZtMb8uyt-9t7Canpm7GfJg4uVg3MLgbvCKxJ4BSu4UgXPz-MYupHS_pIEtlCY8FjlVrXlBLAleUvcBPY2qML9gxpqBrh9s1qfLpCeTZkG-vDjb_Y8X0gXa0OshFrvnoIyHwDc9jmj1X35T0YslyjbQXWQ";
let decoded = Token::from_jwt(token, &dec_key, &None, &None).unwrap();
let decoded = Token::from_jwt(
token,
jsonwebtoken::Algorithm::RS256,
&dec_key,
&None,
&None,
)
.unwrap();
let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" });