feat: support OIDC id_token

- generate JWT id_token in token exchange
- store optional nonce in authorization object
- switch to RS256 algorithm for JWT signature
- add JWKs endpoint to provide OIDC clients with public keys
This commit is contained in:
Matthieu Bessat 2024-12-09 09:38:39 +01:00
parent 4763915812
commit 02e16a7e74
32 changed files with 469 additions and 103 deletions

View file

@ -1,4 +1,5 @@
pub const DEFAULT_DB_PATH: &str = "/var/lib/minauthator/minauthator.db";
pub const DEFAULT_ASSETS_PATH: &str = "/usr/local/lib/minauthator/assets";
pub const DEFAULT_CONFIG_PATH: &str = "/etc/minauthator/config.toml";
pub const DEFAULT_SIGNING_KEY_PATH: &str = "/etc/minauthator/secrets/jwt.key.pem";

View file

@ -1,11 +1,13 @@
use std::{env, fs};
use std::{env, fs, path::Path};
use anyhow::{Result, Context, anyhow};
use fully_pub::fully_pub;
use log::info;
use sqlx::{Pool, Sqlite};
use crate::{
consts::{DEFAULT_CONFIG_PATH, DEFAULT_DB_PATH}, database::prepare_database, models::config::Config, repositories::storage::Storage
consts::{DEFAULT_CONFIG_PATH, DEFAULT_DB_PATH, DEFAULT_SIGNING_KEY_PATH},
database::prepare_database,
models::config::Config,
repositories::storage::Storage
};
/// get server config
@ -26,9 +28,18 @@ struct StartKernelConfig {
#[derive(Debug, Clone)]
#[fully_pub]
struct AppSecrets {
jwt_secret: String
/// RSA keypair (public, private) used to signed the JWT issued by minauthator in PEM conainer format
signing_keypair: (Vec<u8>, Vec<u8>)
}
#[derive(Debug, Clone)]
#[fully_pub]
struct ComputedConfig {
signing_public_key: Vec<u8>,
signing_private_key: Vec<u8>
}
#[derive(Debug, Clone)]
#[fully_pub]
struct KernelContext {
@ -37,6 +48,19 @@ struct KernelContext {
storage: Storage
}
fn get_signing_keypair(key_path: &str) -> Result<(Vec<u8>, Vec<u8>)> {
let key_path = Path::new(key_path);
let pub_key_path = key_path.with_extension("pub");
let private_key: Vec<u8> = fs::read_to_string(&key_path)
.context(format!("Failed to read private key from path {:?}.", key_path))?
.as_bytes().to_vec();
let public_key: Vec<u8> = fs::read_to_string(&pub_key_path)
.context(format!("Failed to read public key from path {:?}.", pub_key_path))?
.as_bytes().to_vec();
Ok((public_key, private_key))
}
pub async fn get_kernel_context(start_config: StartKernelConfig) -> Result<KernelContext> {
env_logger::init();
let _ = dotenvy::dotenv();
@ -50,10 +74,13 @@ pub async fn get_kernel_context(start_config: StartKernelConfig) -> Result<Kerne
let config: Config = get_config(config_path)
.expect("Cannot get config.");
let signing_key_path = config.signing_key.clone().unwrap_or(DEFAULT_SIGNING_KEY_PATH.to_string());
// optionally load dotenv file
let _ = dotenvy::dotenv();
let secrets = AppSecrets {
jwt_secret: env::var("APP_JWT_SECRET")
.context("Expected APP_JWT_SECRET environment variable to exists.")?
signing_keypair: get_signing_keypair(&signing_key_path)?
};
Ok(KernelContext {

View file

@ -25,6 +25,8 @@ struct Authorization {
client_id: String,
scopes: Json<Vec<AuthorizationScope>>,
nonce: Option<String>,
/// defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
code: String,
last_used_at: Option<DateTime<Utc>>,

View file

@ -66,11 +66,6 @@ struct Role {
struct Config {
instance: InstanceConfig,
applications: Vec<Application>,
roles: Vec<Role>
}
#[derive(Debug, Clone)]
#[fully_pub]
struct AppSecrets {
jwt_secret: String
roles: Vec<Role>,
signing_key: Option<String>
}

View file

@ -14,7 +14,7 @@ enum UserStatus {
Active
}
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug)]
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug, Clone)]
#[fully_pub]
struct User {
/// uuid