From 02e16a7e74ace1f1d0604a68ead45aae971fd6a7 Mon Sep 17 00:00:00 2001 From: Matthieu Bessat Date: Mon, 9 Dec 2024 09:38:39 +0100 Subject: [PATCH] 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 --- .env | 1 - .swp | Bin 0 -> 77824 bytes Cargo.lock | 49 +++++++++- TODO.md | 5 + config.toml | 19 +++- justfile | 20 ++-- lib/http_server/Cargo.toml | 9 ++ .../controllers/api/oauth2/access_token.rs | 39 ++++++-- .../src/controllers/api/openid/keys.rs | 45 +++++++++ .../src/controllers/api/openid/mod.rs | 1 + .../src/controllers/api/openid/well_known.rs | 10 +- .../src/controllers/api/read_user.rs | 6 +- .../src/controllers/ui/authorize.rs | 18 ++-- lib/http_server/src/controllers/ui/login.rs | 4 +- lib/http_server/src/middlewares/app_auth.rs | 20 ++-- lib/http_server/src/middlewares/user_auth.rs | 26 +++--- lib/http_server/src/router.rs | 3 +- lib/http_server/src/services/oauth2.rs | 13 ++- lib/http_server/src/services/session.rs | 27 ++++-- lib/http_server/src/token_claims.rs | 88 ++++++++++++++---- lib/kernel/src/consts.rs | 1 + lib/kernel/src/context.rs | 39 ++++++-- lib/kernel/src/models/authorization.rs | 2 + lib/kernel/src/models/config.rs | 9 +- lib/kernel/src/models/user.rs | 2 +- migrations/all.sql | 1 + tests/hurl_integration/oidc_core/config.toml | 58 ++++++++++++ tests/hurl_integration/oidc_core/init_db.sh | 11 +++ tests/hurl_integration/oidc_core/main.hurl | 41 ++++++++ tests/hurl_integration/scenario_1/config.toml | 2 + tests/hurl_integration/scenario_1/main.hurl | 2 +- .../user_invitation_scenario/config.toml | 1 + 32 files changed, 469 insertions(+), 103 deletions(-) create mode 100644 .swp create mode 100644 lib/http_server/src/controllers/api/openid/keys.rs create mode 100644 tests/hurl_integration/oidc_core/config.toml create mode 100755 tests/hurl_integration/oidc_core/init_db.sh create mode 100644 tests/hurl_integration/oidc_core/main.hurl diff --git a/.env b/.env index 8c4c540..e69de29 100644 --- a/.env +++ b/.env @@ -1 +0,0 @@ -APP_JWT_SECRET=bc1996ea-5464-424a-9a38-5604f2bc865a diff --git a/.swp b/.swp new file mode 100644 index 0000000000000000000000000000000000000000..6f58751739bf717a577f867cfa00d136ffe82c4f GIT binary patch literal 77824 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 Ha5DaXaZIwb literal 0 HcmV?d00001 diff --git a/Cargo.lock b/Cargo.lock index fca0ec9..9b04e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -966,11 +972,13 @@ dependencies = [ "chrono", "env_logger", "fully_pub", + "jsonwebkey-convert", "jsonwebtoken", "kernel", "log", "minijinja", "minijinja-embed", + "pem 3.0.4", "serde", "serde_json", "serde_urlencoded", @@ -1238,6 +1246,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebkey-convert" +version = "0.3.0" +dependencies = [ + "base64 0.13.1", + "lazy_static", + "num-bigint", + "pem 0.8.3", + "serde", + "serde_json", + "simple_asn1 0.5.4", + "thiserror 1.0.69", +] + [[package]] name = "jsonwebtoken" version = "9.3.0" @@ -1246,11 +1268,11 @@ checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ "base64 0.21.7", "js-sys", - "pem", + "pem 3.0.4", "ring", "serde", "serde_json", - "simple_asn1", + "simple_asn1 0.6.2", ] [[package]] @@ -1561,6 +1583,17 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + [[package]] name = "pem" version = "3.0.4" @@ -1921,6 +1954,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "simple_asn1" version = "0.6.2" diff --git a/TODO.md b/TODO.md index 6d8df20..5771e72 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,8 @@ # TODO +- [ ] better OIDC support +- [ ] better support of `profile` `openid` `email` `roles` scopes + - [ ] i18n strings in the http website. - [ ] Instance customization support @@ -50,3 +53,5 @@ - [X] basic docker setup - [ ] make `docker stop` working (handle SIGTERM/SIGINT) - [ ] implement docker secrets. https://docs.docker.com/engine/swarm/secrets/ + +- [ ] Find a minimal OpenID client implementation like Listmonk but a little bit more mature diff --git a/config.toml b/config.toml index 2b97bd9..12f31b4 100644 --- a/config.toml +++ b/config.toml @@ -1,8 +1,23 @@ +signing_key = "tmp/secrets/signing.key" + [instance] -base_uri = "http://localhost:8085" -name = "Example org" +base_uri = "https://auth.fictive.org" +name = "Fictive's auth" logo_uri = "https://example.org/logo.png" +[[applications]] +slug = "listmonk" +name = "Listmonk" +description = "Newsletter tool." +client_id = "da2120b4-635d-4eb5-8b2f-dbae89f6a6e9" +client_secret = "59da2291-8999-40e2-afe9-a54ac7cd0a94" +login_uri = "https://lists.fictive.org" +allowed_redirect_uris = [ + "https://lists.fictive.org/auth/oidc", +] +visibility = "Internal" +authorize_flow = "Implicit" + [[applications]] slug = "demo_app" name = "Demo app" diff --git a/justfile b/justfile index c3c20d9..70b755d 100644 --- a/justfile +++ b/justfile @@ -2,17 +2,17 @@ export RUST_BACKTRACE := "1" export RUST_LOG := "trace" export CONTEXT_ARGS := "--config config.toml --database tmp/dbs/minauthator.db --static-assets ./assets" -watch-server: - cargo-watch -x "run --bin minauthator-server -- $CONTEXT_ARGS" +watch-server *args: + cargo-watch -x "run --bin minauthator-server -- $CONTEXT_ARGS {{args}}" -server: - cargo run --bin minauthator-server -- $CONTEXT_ARGS +server *args: + cargo run --bin minauthator-server -- $CONTEXT_ARGS {{args}} -admin: - cargo run --bin minauthator-admin -- $CONTEXT_ARGS +admin *args: + cargo run --bin minauthator-admin -- $CONTEXT_ARGS {{args}} -docker-build: - docker build -t lefuturiste/minauthator . +docker-build *args: + docker build -t lefuturiste/minauthator {{args}} . docker-init-db: docker run \ @@ -28,6 +28,6 @@ docker-run: -v minauthator-db:/var/lib/minauthator \ lefuturiste/minauthator -init-db: - sqlite3 -echo tmp/dbs/minauthator.db < migrations/all.sql +init-db *args: + sqlite3 {{args}} tmp/dbs/minauthator.db < migrations/all.sql diff --git a/lib/http_server/Cargo.toml b/lib/http_server/Cargo.toml index 260d58c..7fc709b 100644 --- a/lib/http_server/Cargo.toml +++ b/lib/http_server/Cargo.toml @@ -43,6 +43,15 @@ argh = { workspace = true } sqlx = { workspace = true } uuid = { workspace = true } url = { workspace = true } +pem = "3.0.4" + +# For now, we test if it's viable, and later we will fork it to fix the build (cf. issue +# https://github.com/informationsea/jsonwebkey-rs#1 ) +[dependencies.jsonwebkey-convert] +path = "/home/mbess/workspace/foss/rust_libs/jsonwebkey-rs/jsonwebkey-convert" +features = ["simple_asn1", "pem"] + +pem = "3.0.4" [build-dependencies] minijinja-embed = "2.3.1" diff --git a/lib/http_server/src/controllers/api/oauth2/access_token.rs b/lib/http_server/src/controllers/api/oauth2/access_token.rs index 1da6993..396ea96 100644 --- a/lib/http_server/src/controllers/api/oauth2/access_token.rs +++ b/lib/http_server/src/controllers/api/oauth2/access_token.rs @@ -4,9 +4,9 @@ use fully_pub::fully_pub; use log::error; use serde::{Deserialize, Serialize}; -use kernel::models::authorization::Authorization; +use kernel::{models::authorization::Authorization, repositories::users::get_user_by_id}; use crate::{ - services::{app_session::AppClientSession, session::create_token}, token_claims::AppUserTokenClaims, AppState + services::{app_session::AppClientSession, session::create_token}, token_claims::{OAuth2AccessTokenClaims, OIDCIdTokenClaims}, AppState }; const AUTHORIZATION_CODE_TTL_SECONDS: i64 = 120; @@ -22,6 +22,7 @@ struct AccessTokenRequestParams { #[derive(Serialize, Deserialize)] #[fully_pub] struct AccessTokenResponse { + id_token: String, access_token: String, token_type: String, expires_in: u64 @@ -60,6 +61,7 @@ pub async fn get_access_token( ).into_response(); } }; + // 2.2. Validate that the authorization code is not expired let is_code_valid = authorization.last_used_at .map_or(false, |ts| { @@ -72,19 +74,38 @@ pub async fn get_access_token( ).into_response(); } - // 3. Generate JWT for oauth2 client user session - let jwt = create_token( + // 2.3. Fetch user resource owner + let user = get_user_by_id(&app_state.db, &authorization.user_id) + .await + .expect("Expected to get user from authorization."); + + // 3.1. Generate JWT for OAuth2 client user session + let access_token_jwt = create_token( + &app_state.config, &app_state.secrets, - AppUserTokenClaims::new( - &app_client_session.client_id, - &authorization.user_id, + OAuth2AccessTokenClaims::new( + &app_state.config, + &user, authorization.scopes.to_vec() ) ); + // 3.2. Generate id_token for OIDC client + let id_token_claims = OIDCIdTokenClaims::new( + &app_state.config, + &app_client_session.client_id, + user.clone(), + authorization.nonce.clone() + ); + let id_token_jwt = create_token( + &app_state.config, + &app_state.secrets, + id_token_claims + ); // 4. return JWT let access_token_res = AccessTokenResponse { - access_token: jwt, - token_type: "jwt".to_string(), + id_token: id_token_jwt, + access_token: access_token_jwt, + token_type: "Bearer".to_string(), expires_in: 3600 }; Json(access_token_res).into_response() diff --git a/lib/http_server/src/controllers/api/openid/keys.rs b/lib/http_server/src/controllers/api/openid/keys.rs new file mode 100644 index 0000000..8943483 --- /dev/null +++ b/lib/http_server/src/controllers/api/openid/keys.rs @@ -0,0 +1,45 @@ +use jsonwebkey_convert::RSAPublicKey; +use jsonwebkey_convert::der::FromPem; + +use axum::{extract::State, response::IntoResponse, Json}; +use fully_pub::fully_pub; +use serde::Serialize; + +use crate::AppState; + +// /// JSON Web Key +// /// @See https://www.rfc-editor.org/rfc/rfc7517.html +// #[derive(Serialize)] +// #[fully_pub] +// struct RsaJWK { +// #[serde(rename = "use")] +// utilisation: String, +// alg: String, +// kid: String, +// #[serde(rename = "modulus")] +// modulus: String, +// exp: String +// } + +/// JSON Web Key set +/// @See https://www.rfc-editor.org/rfc/rfc7517.html +#[derive(Serialize)] +#[fully_pub] +struct JWKs { + keys: Vec +} + +pub async fn get_signing_public_keys( + State(app_state): State, +) -> impl IntoResponse { + let pem_data = app_state.secrets.signing_keypair.0; + + // extract modulus and exp number from ASN.1 encoded PCKS 1 package + let rsa_jwk = RSAPublicKey::from_pem(pem_data) + .expect("Expected to decode PEM public key"); + dbg!(&rsa_jwk); + + Json(JWKs { + keys: vec![rsa_jwk] + }).into_response() +} diff --git a/lib/http_server/src/controllers/api/openid/mod.rs b/lib/http_server/src/controllers/api/openid/mod.rs index 1ab9853..063a9c9 100644 --- a/lib/http_server/src/controllers/api/openid/mod.rs +++ b/lib/http_server/src/controllers/api/openid/mod.rs @@ -1 +1,2 @@ pub mod well_known; +pub mod keys; diff --git a/lib/http_server/src/controllers/api/openid/well_known.rs b/lib/http_server/src/controllers/api/openid/well_known.rs index 54daf3e..6b2d0e8 100644 --- a/lib/http_server/src/controllers/api/openid/well_known.rs +++ b/lib/http_server/src/controllers/api/openid/well_known.rs @@ -6,6 +6,8 @@ use strum::IntoEnumIterator; use crate::AppState; +/// Manifest used by OpenID Connect clients +/// @See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata #[derive(Serialize)] #[fully_pub] struct WellKnownOpenIdConfiguration { @@ -15,7 +17,9 @@ struct WellKnownOpenIdConfiguration { userinfo_endpoint: String, scopes_supported: Vec, response_types_supported: Vec, - token_endpoint_auth_methods_supported: Vec + token_endpoint_auth_methods_supported: Vec, + id_token_signing_alg_values_supported: Vec, + jwks_uri: String } pub async fn get_well_known_openid_configuration( @@ -30,5 +34,9 @@ pub async fn get_well_known_openid_configuration( scopes_supported: AuthorizationScope::iter().map(|v| v.to_string()).collect(), response_types_supported: vec!["code".into()], token_endpoint_auth_methods_supported: vec!["client_secret_basic".into()], + id_token_signing_alg_values_supported: vec!["RS256".into()], + jwks_uri: format!("{}/.well-known/jwks", base_url) + // jwks_uri: + // subject_types_supported }) } diff --git a/lib/http_server/src/controllers/api/read_user.rs b/lib/http_server/src/controllers/api/read_user.rs index 4b9e7c1..465a307 100644 --- a/lib/http_server/src/controllers/api/read_user.rs +++ b/lib/http_server/src/controllers/api/read_user.rs @@ -2,7 +2,7 @@ use axum::{extract::State, response::IntoResponse, Extension, Json}; use fully_pub::fully_pub; use serde::Serialize; -use crate::{token_claims::AppUserTokenClaims, AppState}; +use crate::{token_claims::OAuth2AccessTokenClaims, AppState}; use kernel::models::user::User; #[derive(Serialize)] @@ -18,11 +18,11 @@ struct ReadUserBasicExtract { pub async fn read_user_basic( State(app_state): State, - Extension(token_claims): Extension, + Extension(token_claims): Extension, ) -> impl IntoResponse { // 1. This handler require app user authentification (JWT) let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1") - .bind(&token_claims.user_id) + .bind(&token_claims.sub) .fetch_one(&app_state.db.0) .await .expect("To get user from claim"); diff --git a/lib/http_server/src/controllers/ui/authorize.rs b/lib/http_server/src/controllers/ui/authorize.rs index f6016c9..923d8a3 100644 --- a/lib/http_server/src/controllers/ui/authorize.rs +++ b/lib/http_server/src/controllers/ui/authorize.rs @@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; -use kernel::{ - models::{authorization::Authorization, config::AppAuthorizeFlow} -}; +use kernel::models::{authorization::Authorization, config::AppAuthorizeFlow}; use utils::get_random_alphanumerical; use crate::{ renderer::TemplateRenderer, services::oauth2::{parse_scope, verify_redirect_uri}, token_claims::UserTokenClaims, AppState @@ -25,6 +23,7 @@ struct AuthorizationParams { redirect_uri: String, /// An opaque value used by the client to maintain state between the request and callback state: String, + nonce: Option } fn redirect_to_client( @@ -34,7 +33,7 @@ fn redirect_to_client( let target_url = format!("{}?code={}&state={}", authorization_params.redirect_uri, authorization_code, - authorization_params.state, + authorization_params.state ); debug!("Redirecting to {}", target_url); @@ -56,6 +55,7 @@ pub async fn authorize_form( query_params: Query ) -> impl IntoResponse { let Query(authorization_params) = query_params; + dbg!(&authorization_params); // 1. Verify the app details let app = match app_state.config.applications @@ -116,9 +116,10 @@ pub async fn authorize_form( // Create new auth code let authorization_code = get_random_alphanumerical(32); // Update last used timestamp for this authorization - let _result = sqlx::query("UPDATE authorizations SET code = $2, last_used_at = $3 WHERE id = $1") + let _result = sqlx::query("UPDATE authorizations SET code = $2, nonce = $3, last_used_at = $4 WHERE id = $1") .bind(existing_authorization.id) .bind(authorization_code.clone()) + .bind(authorization_params.nonce.clone()) .bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)) .execute(&app_state.db.0) .await.unwrap(); @@ -172,6 +173,7 @@ pub async fn perform_authorize( Extension(token_claims): Extension, Form(authorize_form): Form ) -> impl IntoResponse { + dbg!(&authorize_form); // 1. Get the app details let app = match app_state.config.applications .iter() @@ -203,6 +205,7 @@ pub async fn perform_authorize( client_id: app.client_id.clone(), scopes: sqlx::types::Json(scopes), code: authorization_code.clone(), + nonce: authorize_form.nonce.clone(), last_used_at: Some(Utc::now()), created_at: Utc::now(), }; @@ -210,14 +213,15 @@ pub async fn perform_authorize( // 3. Save authorization in DB with state let res = sqlx::query(" INSERT INTO authorizations - (id, user_id, client_id, scopes, code, last_used_at, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) + (id, user_id, client_id, scopes, code, nonce, last_used_at, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ") .bind(authorization.id.clone()) .bind(authorization.user_id) .bind(authorization.client_id) .bind(authorization.scopes) .bind(authorization.code) + .bind(authorization.nonce) .bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true))) .bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true)) .execute(&app_state.db.0) diff --git a/lib/http_server/src/controllers/ui/login.rs b/lib/http_server/src/controllers/ui/login.rs index 59cce05..68061d4 100644 --- a/lib/http_server/src/controllers/ui/login.rs +++ b/lib/http_server/src/controllers/ui/login.rs @@ -91,8 +91,8 @@ pub async fn perform_login( .await.unwrap(); let jwt_max_age = Duration::days(15); - let claims = UserTokenClaims::new(&user.id, jwt_max_age); - let jwt = create_token(&app_state.secrets, claims); + let claims = UserTokenClaims::new(&app_state.config, &user.id, jwt_max_age); + let jwt = create_token(&app_state.config, &app_state.secrets, claims); // TODO: handle keep_session boolean from form and specify cookie max age only if this setting // is true diff --git a/lib/http_server/src/middlewares/app_auth.rs b/lib/http_server/src/middlewares/app_auth.rs index 3709f8b..c5c4e56 100644 --- a/lib/http_server/src/middlewares/app_auth.rs +++ b/lib/http_server/src/middlewares/app_auth.rs @@ -9,7 +9,7 @@ use utils::parse_basic_auth; use crate::{ services::{app_session::AppClientSession, session::verify_token}, - token_claims::AppUserTokenClaims, + token_claims::OAuth2AccessTokenClaims, AppState }; @@ -102,14 +102,16 @@ pub async fn enforce_jwt_auth_middleware( ); } }; - let token_claims: AppUserTokenClaims = match verify_token(&app_state.secrets, jwt) { - Ok(val) => val, - Err(_e) => { - return Err( - (StatusCode::UNAUTHORIZED, Html("Unauthorized: The provided JWT is invalid.")) - ); - } - }; + let token_claims: OAuth2AccessTokenClaims = + match verify_token(&app_state.config, &app_state.secrets, jwt) { + Ok(val) => val, + Err(_e) => { + dbg!(_e); + return Err( + (StatusCode::UNAUTHORIZED, Html("Unauthorized: The provided JWT is invalid.")) + ); + } + }; req.extensions_mut().insert(token_claims); Ok(next.run(req).await) } diff --git a/lib/http_server/src/middlewares/user_auth.rs b/lib/http_server/src/middlewares/user_auth.rs index 7582eb7..921bdce 100644 --- a/lib/http_server/src/middlewares/user_auth.rs +++ b/lib/http_server/src/middlewares/user_auth.rs @@ -28,18 +28,20 @@ pub async fn auth_middleware( return Ok(next.run(req).await) } }; - let token_claims: UserTokenClaims = match verify_token(&app_state.secrets, jwt) { - Ok(val) => val, - Err(_e) => { - // UserWebGUI: delete invalid JWT cookie - return Err( - ( - cookies.remove(WEB_GUI_JWT_COOKIE_NAME), - Redirect::to(&original_uri.to_string()) - ) - ); - } - }; + let token_claims: UserTokenClaims = + match verify_token(&app_state.config, &app_state.secrets, jwt) { + Ok(val) => val, + Err(_e) => { + dbg!(&_e); + // UserWebGUI: delete invalid JWT cookie + return Err( + ( + cookies.remove(WEB_GUI_JWT_COOKIE_NAME), + Redirect::to(&original_uri.to_string()) + ) + ); + } + }; req.extensions_mut().insert(token_claims); Ok(next.run(req).await) } diff --git a/lib/http_server/src/router.rs b/lib/http_server/src/router.rs index 7901928..fcbd89a 100644 --- a/lib/http_server/src/router.rs +++ b/lib/http_server/src/router.rs @@ -51,7 +51,8 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router .route("/api/user-assets/:asset_id", get(api::public_assets::get_user_asset)); let well_known_routes = Router::new() - .route("/.well-known/openid-configuration", get(api::openid::well_known::get_well_known_openid_configuration)); + .route("/.well-known/openid-configuration", get(api::openid::well_known::get_well_known_openid_configuration)) + .route("/.well-known/jwks", get(api::openid::keys::get_signing_public_keys)); Router::new() .merge(public_routes) diff --git a/lib/http_server/src/services/oauth2.rs b/lib/http_server/src/services/oauth2.rs index ed60954..d528747 100644 --- a/lib/http_server/src/services/oauth2.rs +++ b/lib/http_server/src/services/oauth2.rs @@ -12,9 +12,16 @@ pub fn verify_redirect_uri(app: &Application, input_redirect_uri: &str) -> bool pub fn parse_scope(scope_str: &str) -> Result> { let mut scopes: Vec = vec![]; for part in scope_str.split(' ') { - scopes.push( - AuthorizationScope::from_str(part).context("Cannot parse space-delimited scope.")? - ) + if part == "openid" { + scopes.push(AuthorizationScope::UserReadBasic); + scopes.push(AuthorizationScope::UserReadRoles); + continue; + } + if part == "profile" || part == "email" { + continue; + } + scopes.push(AuthorizationScope::from_str(part).context("Cannot parse space-delimited scope.")?); } + dbg!(&scopes); Ok(scopes) } diff --git a/lib/http_server/src/services/session.rs b/lib/http_server/src/services/session.rs index 018e094..dd97828 100644 --- a/lib/http_server/src/services/session.rs +++ b/lib/http_server/src/services/session.rs @@ -1,24 +1,37 @@ use anyhow::Result; use serde::{de::DeserializeOwned, Serialize}; use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey}; -use kernel::context::AppSecrets; +use kernel::{context::AppSecrets, models::config::Config}; -pub fn create_token(secrets: &AppSecrets, claims: T) -> String { +pub fn create_token( + _config: &Config, + secrets: &AppSecrets, + claims: T +) -> String { let token = encode( - &Header::default(), + &Header::new(Algorithm::RS256), &claims, - &EncodingKey::from_secret(secrets.jwt_secret.as_bytes()) + &EncodingKey::from_rsa_pem(&secrets.signing_keypair.1) + .expect("To build encoding key from signing key.") ).expect("Create token"); token } -pub fn verify_token(secrets: &AppSecrets, jwt: &str) -> Result { +pub fn verify_token( + config: &Config, + secrets: &AppSecrets, + jwt: &str +) -> Result { + let mut validation = Validation::new(Algorithm::RS256); + validation.set_issuer(&[config.instance.base_uri.clone()]); + validation.set_audience(&[config.instance.base_uri.clone()]); let token_data = decode::( jwt, - &DecodingKey::from_secret(secrets.jwt_secret.as_bytes()), - &Validation::new(Algorithm::HS256) + &DecodingKey::from_rsa_pem(&secrets.signing_keypair.0) + .expect("To build decoding key from signing key."), + &validation )?; Ok(token_data.claims) diff --git a/lib/http_server/src/token_claims.rs b/lib/http_server/src/token_claims.rs index 932b4ce..726c4e5 100644 --- a/lib/http_server/src/token_claims.rs +++ b/lib/http_server/src/token_claims.rs @@ -1,6 +1,6 @@ use fully_pub::fully_pub; use jsonwebtoken::get_current_timestamp; -use kernel::models::authorization::AuthorizationScope; +use kernel::models::{authorization::AuthorizationScope, config::Config, user::User}; use serde::{Deserialize, Serialize}; use time::Duration; @@ -12,41 +12,91 @@ struct UserTokenClaims { /// token expiration exp: u64, /// token issuer - iss: String + iss: String, // TODO: add roles + /// token audience + aud: String } impl UserTokenClaims { - pub fn new(user_id: &str, max_age: Duration) -> Self { + pub fn new(config: &Config, user_id: &str, max_age: Duration) -> Self { UserTokenClaims { sub: user_id.into(), exp: get_current_timestamp() + max_age.whole_seconds() as u64, - iss: "Minauthator".into() + iss: config.instance.base_uri.clone(), + aud: config.instance.base_uri.clone(), } } } +/// Access token for OAuth2 defined in RFC 9068 +/// @See https://datatracker.ietf.org/doc/html/rfc9068 #[derive(Debug, Serialize, Deserialize, Clone)] #[fully_pub] -struct AppUserTokenClaims { - /// combined subject - client_id: String, - user_id: String, - scopes: Vec, - /// token expiration +struct OAuth2AccessTokenClaims { + /// Token issuer (URI to the issuer) + iss: String, + /// Audiance (In this case, the audiance is equal to the issuer) + aud: String, + /// End-user id assigned by the issuer (user_id) + sub: String, + /// Token expiration exp: u64, - /// token issuer - iss: String + /// List of OAuth 2 scopes asked by the client + scopes: Vec } -impl AppUserTokenClaims { - pub fn new(client_id: &str, user_id: &str, scopes: Vec) -> Self { - AppUserTokenClaims { - client_id: client_id.into(), - user_id: user_id.into(), - scopes, +impl OAuth2AccessTokenClaims { + pub fn new(config: &Config, user: &User, scopes: Vec) -> Self { + OAuth2AccessTokenClaims { + iss: config.instance.base_uri.clone(), + aud: config.instance.base_uri.clone(), + sub: user.id.clone(), exp: get_current_timestamp() + 86_000, - iss: "Minauth".into() + scopes, + } + } +} + + +/// @See https://openid.net/specs/openid-connect-core-1_0.html#IDToken +/// @See https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims +#[derive(Debug, Serialize, Deserialize, Clone)] +#[fully_pub] +struct OIDCIdTokenClaims { + /// Token expiration + exp: u64, + /// Token issuer (URI to the issuer) + iss: String, + /// Audiance (client_id) + aud: String, + /// End-user id assigned by the issuer (user_id) + sub: String, + /// additional claims + name: Option, + email: Option, + preferred_username: Option, + roles: Vec, + nonce: Option +} + +impl OIDCIdTokenClaims { + pub fn new( + config: &Config, + client_id: &str, + user: User, + nonce: Option + ) -> Self { + OIDCIdTokenClaims { + iss: config.instance.base_uri.clone(), + aud: client_id.into(), + sub: user.id, + exp: get_current_timestamp() + 86_000, + email: user.email, + name: user.full_name, + preferred_username: Some(user.handle), + roles: user.roles.0, + nonce } } } diff --git a/lib/kernel/src/consts.rs b/lib/kernel/src/consts.rs index 31fca20..9bdfaf2 100644 --- a/lib/kernel/src/consts.rs +++ b/lib/kernel/src/consts.rs @@ -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"; diff --git a/lib/kernel/src/context.rs b/lib/kernel/src/context.rs index 0c6e3b1..cb90e0d 100644 --- a/lib/kernel/src/context.rs +++ b/lib/kernel/src/context.rs @@ -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, Vec) } +#[derive(Debug, Clone)] +#[fully_pub] +struct ComputedConfig { + signing_public_key: Vec, + signing_private_key: Vec +} + + #[derive(Debug, Clone)] #[fully_pub] struct KernelContext { @@ -37,6 +48,19 @@ struct KernelContext { storage: Storage } +fn get_signing_keypair(key_path: &str) -> Result<(Vec, Vec)> { + let key_path = Path::new(key_path); + let pub_key_path = key_path.with_extension("pub"); + let private_key: Vec = 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 = 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 { env_logger::init(); let _ = dotenvy::dotenv(); @@ -50,10 +74,13 @@ pub async fn get_kernel_context(start_config: StartKernelConfig) -> Result>, + nonce: Option, + /// defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 code: String, last_used_at: Option>, diff --git a/lib/kernel/src/models/config.rs b/lib/kernel/src/models/config.rs index 57a275a..bf2d311 100644 --- a/lib/kernel/src/models/config.rs +++ b/lib/kernel/src/models/config.rs @@ -66,11 +66,6 @@ struct Role { struct Config { instance: InstanceConfig, applications: Vec, - roles: Vec -} - -#[derive(Debug, Clone)] -#[fully_pub] -struct AppSecrets { - jwt_secret: String + roles: Vec, + signing_key: Option } diff --git a/lib/kernel/src/models/user.rs b/lib/kernel/src/models/user.rs index 9634fa0..355fcdd 100644 --- a/lib/kernel/src/models/user.rs +++ b/lib/kernel/src/models/user.rs @@ -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 diff --git a/migrations/all.sql b/migrations/all.sql index 2f71c3c..adf3327 100644 --- a/migrations/all.sql +++ b/migrations/all.sql @@ -33,6 +33,7 @@ CREATE TABLE authorizations ( client_id TEXT NOT NULL, scopes TEXT, -- json array of app scope (permissions) code TEXT, + nonce TEXT, -- code used to associate client session to id_token last_used_at DATETIME, created_at DATETIME NOT NULL diff --git a/tests/hurl_integration/oidc_core/config.toml b/tests/hurl_integration/oidc_core/config.toml new file mode 100644 index 0000000..df607aa --- /dev/null +++ b/tests/hurl_integration/oidc_core/config.toml @@ -0,0 +1,58 @@ +signing_key = "tmp/secrets/signing.key" + +[instance] +base_uri = "http://localhost:8086" +name = "Example org" +logo_uri = "https://example.org/logo.png" + +[[applications]] +slug = "demo_app" +name = "Demo app" +description = "A super application where you can do everything you want." +client_id = "00000001-0000-0000-0000-000000000001" +client_secret = "dummy_client_secret" +login_uri = "https://localhost:9876" +allowed_redirect_uris = [ + "http://localhost:9090/callback", + "http://localhost:9876/callback" +] +visibility = "Internal" +authorize_flow = "Implicit" + +[[applications]] +slug = "wiki" +name = "Wiki app" +description = "The knowledge base of the exemple org." +client_id = "f9de1885-448d-44bb-8c48-7e985486a8c6" +client_secret = "49c6c16a-0a8a-4981-a60d-5cb96582cc1a" +login_uri = "https://wiki.example.org/login" +allowed_redirect_uris = [ + "https://wiki.example.org/oauth2/callback" +] +visibility = "Public" +authorize_flow = "Implicit" + +[[applications]] +slug = "private_app" +name = "Demo app" +description = "Private app you should never discover" +client_id = "c8a08783-2342-4ce3-a3cb-9dc89b6bdf" +client_secret = "this_is_the_secret" +login_uri = "https://private-app.org" +allowed_redirect_uris = [ + "http://localhost:9091/authorize", +] +visibility = "Private" +authorize_flow = "Implicit" + +[[roles]] +slug = "basic" +name = "Basic" +description = "Basic user" +default = true + +[[roles]] +slug = "admin" +name = "Administrator" +description = "Full power on organization instance" + diff --git a/tests/hurl_integration/oidc_core/init_db.sh b/tests/hurl_integration/oidc_core/init_db.sh new file mode 100755 index 0000000..b57b134 --- /dev/null +++ b/tests/hurl_integration/oidc_core/init_db.sh @@ -0,0 +1,11 @@ +#!/usr/bin/bash + +password_hash="$(echo -n "root" | argon2 salt_06cGGWYDJCZ -e)" +echo $password_hash +SQL=$(cat <