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:
parent
dcd7d7fe87
commit
427ae4550b
9 changed files with 170 additions and 32 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -301,6 +301,7 @@ dependencies = [
|
|||
"humantime",
|
||||
"humantime-serde",
|
||||
"itoa",
|
||||
"jsonwebtoken",
|
||||
"maybe-owned",
|
||||
"rand",
|
||||
"regex",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
)?;
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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!();
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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" });
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue