diff --git a/rauthy-handlers/src/oidc.rs b/rauthy-handlers/src/oidc.rs index 94e6f627..239cd74d 100644 --- a/rauthy-handlers/src/oidc.rs +++ b/rauthy-handlers/src/oidc.rs @@ -2,12 +2,12 @@ use std::ops::Add; use std::time::{SystemTime, UNIX_EPOCH}; use actix_web::cookie::time::OffsetDateTime; -use actix_web::http::header::HeaderValue; +use actix_web::http::header::{HeaderValue, CONTENT_TYPE}; use actix_web::http::{header, StatusCode}; use actix_web::{get, post, web, HttpRequest, HttpResponse, HttpResponseBuilder, ResponseError}; use tracing::debug; -use rauthy_common::constants::{COOKIE_MFA, HEADER_HTML, SESSION_LIFETIME}; +use rauthy_common::constants::{APPLICATION_JSON, COOKIE_MFA, HEADER_HTML, SESSION_LIFETIME}; use rauthy_common::error_response::ErrorResponse; use rauthy_common::utils::build_csp_header; use rauthy_models::app_state::AppState; @@ -17,6 +17,7 @@ use rauthy_models::entity::jwk::{JWKSPublicKey, JwkKeyPair, JWKS}; use rauthy_models::entity::sessions::Session; use rauthy_models::entity::users::User; use rauthy_models::entity::webauthn::WebauthnCookie; +use rauthy_models::entity::well_known::WellKnown; use rauthy_models::language::Language; use rauthy_models::request::{ AuthRequest, LoginRefreshRequest, LoginRequest, LogoutRequest, TokenRequest, @@ -654,11 +655,13 @@ pub async fn get_userinfo( ), )] #[get("/.well-known/openid-configuration")] -pub async fn get_well_known(data: web::Data) -> HttpResponse { - HttpResponse::Ok() +pub async fn get_well_known(data: web::Data) -> Result { + let wk = WellKnown::json(&data).await?; + Ok(HttpResponse::Ok() + .insert_header((CONTENT_TYPE, APPLICATION_JSON)) .insert_header(( header::ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_str("*").unwrap(), )) - .json(&data.well_known) + .body(wk)) } diff --git a/rauthy-handlers/src/openapi.rs b/rauthy-handlers/src/openapi.rs index 137a6e67..a41f835b 100644 --- a/rauthy-handlers/src/openapi.rs +++ b/rauthy-handlers/src/openapi.rs @@ -4,7 +4,7 @@ use crate::{ use actix_web::web; use rauthy_common::constants::{PROXY_MODE, RAUTHY_VERSION}; use rauthy_common::error_response::{ErrorResponse, ErrorResponseType}; -use rauthy_models::app_state::{AppState, WellKnown}; +use rauthy_models::app_state::AppState; use rauthy_models::events::event; use rauthy_models::language; use rauthy_models::ListenScheme; @@ -137,6 +137,7 @@ use utoipa::{openapi, OpenApi}; entity::webauthn::WebauthnAdditionalData, entity::webauthn::WebauthnLoginReq, entity::webauthn::WebauthnServiceReq, + entity::well_known::WellKnown, event::EventLevel, ErrorResponse, @@ -205,8 +206,6 @@ use utoipa::{openapi, OpenApi}; rauthy_models::JktClaim, token_set::TokenSet, - - WellKnown, ), ), tags( diff --git a/rauthy-main/tests/handler_generic.rs b/rauthy-main/tests/handler_generic.rs index c51979fa..56c71def 100644 --- a/rauthy-main/tests/handler_generic.rs +++ b/rauthy-main/tests/handler_generic.rs @@ -1,6 +1,6 @@ use crate::common::{get_backend_url, get_issuer}; use pretty_assertions::assert_eq; -use rauthy_models::app_state::WellKnown; +use rauthy_models::entity::well_known::WellKnown; use std::error::Error; mod common; diff --git a/rauthy-models/src/app_state.rs b/rauthy-models/src/app_state.rs index 5a8d7604..8d136bf5 100644 --- a/rauthy-models/src/app_state.rs +++ b/rauthy-models/src/app_state.rs @@ -12,7 +12,6 @@ use argon2::Params; use rauthy_common::constants::{DATABASE_URL, DB_TYPE, DEV_MODE, HA_MODE, PROXY_MODE}; use rauthy_common::DbType; use regex::Regex; -use serde::{Deserialize, Serialize}; use sqlx::pool::PoolOptions; use sqlx::ConnectOptions; use std::collections::HashMap; @@ -24,7 +23,6 @@ use tokio::sync::mpsc; use tokio::time::sleep; use tracing::log::LevelFilter; use tracing::{debug, error, info, warn}; -use utoipa::ToSchema; use webauthn_rs::prelude::Url; use webauthn_rs::Webauthn; @@ -49,7 +47,6 @@ pub struct AppState { pub listen_addr: String, pub listen_scheme: ListenScheme, pub refresh_grace_time: u32, - pub well_known: WellKnown, pub session_lifetime: u32, pub session_timeout: u32, pub ml_lt_pwd_first: u32, @@ -146,7 +143,6 @@ impl AppState { "http" }; let issuer = format!("{}://{}/auth/v1", issuer_scheme, public_url); - let well_known = WellKnown::new(&issuer); let session_lifetime = env::var("SESSION_LIFETIME") .unwrap_or_else(|_| String::from("14400")) @@ -199,7 +195,6 @@ impl AppState { listen_addr, listen_scheme, refresh_grace_time, - well_known, session_lifetime, session_timeout, ml_lt_pwd_first, @@ -443,97 +438,3 @@ pub struct Argon2Params { pub struct Caches { pub ha_cache_config: redhac::CacheConfig, } - -/// The struct for the `.well-known` endpoint for automatic OIDC discovery -#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] -pub struct WellKnown { - pub issuer: String, - pub authorization_endpoint: String, - pub token_endpoint: String, - pub introspection_endpoint: String, - pub userinfo_endpoint: String, - pub end_session_endpoint: String, - pub jwks_uri: String, - // pub registration_endpoint: String, - // pub check_session_iframe: String, - pub grant_types_supported: Vec, - pub response_types_supported: Vec, - pub id_token_signing_alg_values_supported: Vec, - pub token_endpoint_auth_signing_alg_values_supported: Vec, - pub claims_supported: Vec, - pub scopes_supported: Vec, - pub code_challenge_methods_supported: Vec, - pub dpop_signing_alg_values_supported: Vec, -} - -impl WellKnown { - pub fn new(issuer: &str) -> Self { - let authorization_endpoint = format!("{}/oidc/authorize", issuer); - let token_endpoint = format!("{}/oidc/token", issuer); - let introspection_endpoint = format!("{}/oidc/tokenInfo", issuer); - let userinfo_endpoint = format!("{}/oidc/userinfo", issuer); - let end_session_endpoint = format!("{}/oidc/userinfo", issuer); - let jwks_uri = format!("{}/oidc/certs", issuer); - let grant_types_supported = vec![ - String::from("authorization_code"), - String::from("client_credentials"), - String::from("password"), - String::from("refresh_token"), - ]; - let response_types_supported = vec![String::from("code")]; - let id_token_signing_alg_values_supported = vec![ - String::from("RS256"), - String::from("RS384"), - String::from("RS512"), - String::from("EdDSA"), - ]; - let token_endpoint_auth_signing_alg_values_supported = vec![ - String::from("RS256"), - String::from("RS384"), - String::from("RS512"), - String::from("EdDSA"), - ]; - let claims_supported = vec![ - String::from("aud"), - String::from("sub"), - String::from("iss"), - String::from("name"), - String::from("given_name"), - String::from("family_name"), - String::from("preferred_username"), - String::from("email"), - ]; - let scopes_supported = vec![ - String::from("openid"), - String::from("profile"), - String::from("email"), - String::from("roles"), - String::from("groups"), - ]; - let code_challenge_methods_supported = vec![String::from("plain"), String::from("S256")]; - let dpop_signing_alg_values_supported = vec![ - String::from("RS256"), - String::from("RS384"), - String::from("RS512"), - String::from("EdDSA"), - ]; - - Self { - issuer: String::from(issuer), - authorization_endpoint, - token_endpoint, - introspection_endpoint, - userinfo_endpoint, - end_session_endpoint, - jwks_uri, - grant_types_supported, - response_types_supported, - id_token_signing_alg_values_supported, - token_endpoint_auth_signing_alg_values_supported, - claims_supported, - scopes_supported, - code_challenge_methods_supported, - dpop_signing_alg_values_supported, - } - } -} diff --git a/rauthy-models/src/entity/mod.rs b/rauthy-models/src/entity/mod.rs index 67acb003..810a183c 100644 --- a/rauthy-models/src/entity/mod.rs +++ b/rauthy-models/src/entity/mod.rs @@ -23,6 +23,7 @@ pub mod sessions; pub mod user_attr; pub mod users; pub mod webauthn; +pub mod well_known; pub async fn is_db_alive(db: &DbPool) -> bool { query("SELECT 1").execute(db).await.is_ok() diff --git a/rauthy-models/src/entity/scopes.rs b/rauthy-models/src/entity/scopes.rs index 07645499..ee646e6f 100644 --- a/rauthy-models/src/entity/scopes.rs +++ b/rauthy-models/src/entity/scopes.rs @@ -14,6 +14,7 @@ use rauthy_common::utils::new_store_id; use crate::app_state::{AppState, DbTxn}; use crate::entity::clients::Client; use crate::entity::user_attr::UserAttrConfigEntity; +use crate::entity::well_known::WellKnown; use crate::request::ScopeRequest; #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] @@ -82,6 +83,8 @@ impl Scope { ) .await?; + WellKnown::rebuild(data).await?; + Ok(new_scope) } @@ -144,6 +147,8 @@ impl Scope { ) .await?; + WellKnown::rebuild(data).await?; + Ok(()) } @@ -210,7 +215,7 @@ impl Scope { let mut txn = data.db.begin().await?; // if the name has changed, we need to update all connected clients - if scope.name != scope_req.scope { + let is_name_update = if scope.name != scope_req.scope { // find all clients with the old_name assigned let mut clients = vec![]; Client::find_all(data) @@ -240,7 +245,11 @@ impl Scope { for client in clients { client.save(data, Some(&mut txn)).await?; } - } + + true + } else { + false + }; debug!("scope_req: {:?}", scope_req); // check configured custom attributes and clean them up @@ -289,6 +298,10 @@ impl Scope { ) .await?; + if is_name_update { + WellKnown::rebuild(data).await?; + } + Ok(new_scope) } diff --git a/rauthy-models/src/entity/well_known.rs b/rauthy-models/src/entity/well_known.rs new file mode 100644 index 00000000..2639a25b --- /dev/null +++ b/rauthy-models/src/entity/well_known.rs @@ -0,0 +1,154 @@ +use crate::app_state::AppState; +use crate::entity::scopes::Scope; +use actix_web::web; +use rauthy_common::constants::CACHE_NAME_12HR; +use rauthy_common::error_response::ErrorResponse; +use redhac::{cache_get, cache_get_from, cache_get_value, cache_put}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// The struct for the `.well-known` endpoint for automatic OIDC discovery +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct WellKnown { + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub introspection_endpoint: String, + pub userinfo_endpoint: String, + pub end_session_endpoint: String, + pub jwks_uri: String, + // #[serde(skip_serializing_if = "Option::is_none")] + // pub registration_endpoint: Option, + // pub check_session_iframe: String, + pub grant_types_supported: Vec, + pub response_types_supported: Vec, + pub id_token_signing_alg_values_supported: Vec, + pub token_endpoint_auth_signing_alg_values_supported: Vec, + pub claims_supported: Vec, + pub scopes_supported: Vec, + pub code_challenge_methods_supported: Vec, + pub dpop_signing_alg_values_supported: Vec, +} + +const IDX: &str = ".well-known"; + +impl WellKnown { + pub async fn json(data: &web::Data) -> Result { + if let Some(wk) = cache_get!( + String, + CACHE_NAME_12HR.to_string(), + IDX.to_string(), + &data.caches.ha_cache_config, + false + ) + .await? + { + Ok(wk) + } else { + let scopes = Scope::find_all(data) + .await? + .into_iter() + .map(|s| s.name) + .collect::>(); + let slf = Self::new(&data.issuer, scopes); + let json = serde_json::to_string(&slf).unwrap(); + + cache_put( + CACHE_NAME_12HR.to_string(), + IDX.to_string(), + &data.caches.ha_cache_config, + &json, + ) + .await?; + + Ok(json) + } + } + + /// Rebuilds the WellKnown, serializes it as json and updates it inside the cache. + /// Should be called after any update on the Scopes. + pub async fn rebuild(data: &web::Data) -> Result<(), ErrorResponse> { + let scopes = Scope::find_all(data) + .await? + .into_iter() + .map(|s| s.name) + .collect::>(); + let slf = Self::new(&data.issuer, scopes); + let json = serde_json::to_string(&slf).unwrap(); + + cache_put( + CACHE_NAME_12HR.to_string(), + IDX.to_string(), + &data.caches.ha_cache_config, + &json, + ) + .await?; + + Ok(()) + } +} + +impl WellKnown { + pub fn new(issuer: &str, scopes_supported: Vec) -> Self { + let authorization_endpoint = format!("{}/oidc/authorize", issuer); + let token_endpoint = format!("{}/oidc/token", issuer); + let introspection_endpoint = format!("{}/oidc/tokenInfo", issuer); + let userinfo_endpoint = format!("{}/oidc/userinfo", issuer); + let end_session_endpoint = format!("{}/oidc/userinfo", issuer); + let jwks_uri = format!("{}/oidc/certs", issuer); + let grant_types_supported = vec![ + "authorization_code".to_string(), + "client_credentials".to_string(), + "password".to_string(), + "refresh_token".to_string(), + ]; + let response_types_supported = vec!["code".to_string()]; + let id_token_signing_alg_values_supported = vec![ + "RS256".to_string(), + "RS384".to_string(), + "RS512".to_string(), + "EdDSA".to_string(), + ]; + let token_endpoint_auth_signing_alg_values_supported = vec![ + "RS256".to_string(), + "RS384".to_string(), + "RS512".to_string(), + "EdDSA".to_string(), + ]; + let claims_supported = vec![ + "aud".to_string(), + "sub".to_string(), + "iss".to_string(), + "name".to_string(), + "given_name".to_string(), + "family_name".to_string(), + "preferred_username".to_string(), + "email".to_string(), + ]; + let code_challenge_methods_supported = vec!["plain".to_string(), "S256".to_string()]; + let dpop_signing_alg_values_supported = vec![ + "RS256".to_string(), + "RS384".to_string(), + "RS512".to_string(), + "EdDSA".to_string(), + ]; + + WellKnown { + issuer: String::from(issuer), + authorization_endpoint, + token_endpoint, + introspection_endpoint, + userinfo_endpoint, + end_session_endpoint, + jwks_uri, + grant_types_supported, + response_types_supported, + id_token_signing_alg_values_supported, + token_endpoint_auth_signing_alg_values_supported, + claims_supported, + scopes_supported, + code_challenge_methods_supported, + dpop_signing_alg_values_supported, + } + } +}