1
0
Fork 1
Spiegel von https://github.com/dani-garcia/vaultwarden.git synchronisiert 2025-02-07 11:17:02 +01:00

Add wrapper type OIDCCode OIDCState OIDCIdentifier

Dieser Commit ist enthalten in:
Timshel 2025-01-10 17:38:40 +01:00
Ursprung 2f4d2daec6
Commit 16c230e570
4 geänderte Dateien mit 111 neuen und 30 gelöschten Zeilen

Datei anzeigen

@ -23,7 +23,9 @@ use crate::{
auth::{AuthMethod, ClientHeaders, ClientIp}, auth::{AuthMethod, ClientHeaders, ClientIp},
db::{models::*, DbConn}, db::{models::*, DbConn},
error::MapResult, error::MapResult,
mail, sso, util, CONFIG, mail, sso,
sso::{OIDCCode, OIDCState},
util, CONFIG,
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
@ -968,7 +970,7 @@ fn prevalidate() -> JsonResult {
} }
#[get("/connect/oidc-signin?<code>&<state>", rank = 1)] #[get("/connect/oidc-signin?<code>&<state>", rank = 1)]
async fn oidcsignin(code: String, state: String, conn: DbConn) -> ApiResult<Redirect> { async fn oidcsignin(code: OIDCCode, state: String, conn: DbConn) -> ApiResult<Redirect> {
oidcsignin_redirect( oidcsignin_redirect(
state, state,
|decoded_state| sso::OIDCCodeWrapper::Ok { |decoded_state| sso::OIDCCodeWrapper::Ok {
@ -1005,16 +1007,10 @@ async fn oidcsignin_error(
// iss and scope parameters are needed for redirection to work on IOS. // iss and scope parameters are needed for redirection to work on IOS.
async fn oidcsignin_redirect( async fn oidcsignin_redirect(
base64_state: String, base64_state: String,
wrapper: impl FnOnce(String) -> sso::OIDCCodeWrapper, wrapper: impl FnOnce(OIDCState) -> sso::OIDCCodeWrapper,
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Redirect> { ) -> ApiResult<Redirect> {
let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) { let state = sso::deocde_state(base64_state)?;
Ok(vec) => match String::from_utf8(vec) {
Ok(valid) => valid,
Err(_) => err!(format!("Invalid utf8 chars in {base64_state} after base64 decoding")),
},
Err(_) => err!(format!("Failed to decode {base64_state} using base64")),
};
let code = sso::encode_code_claims(wrapper(state.clone())); let code = sso::encode_code_claims(wrapper(state.clone()));
let nonce = match SsoNonce::find(&state, conn).await { let nonce = match SsoNonce::find(&state, conn).await {
@ -1050,7 +1046,7 @@ struct AuthorizeData {
response_type: Option<String>, response_type: Option<String>,
#[allow(unused)] #[allow(unused)]
scope: Option<String>, scope: Option<String>,
state: String, state: OIDCState,
#[allow(unused)] #[allow(unused)]
code_challenge: Option<String>, code_challenge: Option<String>,
#[allow(unused)] #[allow(unused)]

Datei anzeigen

@ -3,14 +3,14 @@ use chrono::{NaiveDateTime, Utc};
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::db::{DbConn, DbPool}; use crate::db::{DbConn, DbPool};
use crate::error::MapResult; use crate::error::MapResult;
use crate::sso::NONCE_EXPIRATION; use crate::sso::{OIDCState, NONCE_EXPIRATION};
db_object! { db_object! {
#[derive(Identifiable, Queryable, Insertable)] #[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = sso_nonce)] #[diesel(table_name = sso_nonce)]
#[diesel(primary_key(state))] #[diesel(primary_key(state))]
pub struct SsoNonce { pub struct SsoNonce {
pub state: String, pub state: OIDCState,
pub nonce: String, pub nonce: String,
pub verifier: Option<String>, pub verifier: Option<String>,
pub redirect_uri: String, pub redirect_uri: String,
@ -20,7 +20,7 @@ db_object! {
/// Local methods /// Local methods
impl SsoNonce { impl SsoNonce {
pub fn new(state: String, nonce: String, verifier: Option<String>, redirect_uri: String) -> Self { pub fn new(state: OIDCState, nonce: String, verifier: Option<String>, redirect_uri: String) -> Self {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
SsoNonce { SsoNonce {
@ -53,7 +53,7 @@ impl SsoNonce {
} }
} }
pub async fn delete(state: &str, conn: &mut DbConn) -> EmptyResult { pub async fn delete(state: &OIDCState, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: { db_run! { conn: {
diesel::delete(sso_nonce::table.filter(sso_nonce::state.eq(state))) diesel::delete(sso_nonce::table.filter(sso_nonce::state.eq(state)))
.execute(conn) .execute(conn)
@ -61,7 +61,7 @@ impl SsoNonce {
}} }}
} }
pub async fn find(state: &str, conn: &DbConn) -> Option<Self> { pub async fn find(state: &OIDCState, conn: &DbConn) -> Option<Self> {
let oldest = Utc::now().naive_utc() - *NONCE_EXPIRATION; let oldest = Utc::now().naive_utc() - *NONCE_EXPIRATION;
db_run! { conn: { db_run! { conn: {
sso_nonce::table sso_nonce::table

Datei anzeigen

@ -10,6 +10,7 @@ use crate::{
crypto, crypto,
db::DbConn, db::DbConn,
error::MapResult, error::MapResult,
sso::OIDCIdentifier,
util::{format_date, get_uuid, retry}, util::{format_date, get_uuid, retry},
CONFIG, CONFIG,
}; };
@ -77,7 +78,7 @@ db_object! {
#[diesel(primary_key(user_uuid))] #[diesel(primary_key(user_uuid))]
pub struct SsoUser { pub struct SsoUser {
pub user_uuid: UserId, pub user_uuid: UserId,
pub identifier: String, pub identifier: OIDCIdentifier,
} }
} }

Datei anzeigen

@ -1,4 +1,5 @@
use chrono::Utc; use chrono::Utc;
use derive_more::{AsRef, Deref, Display, From};
use regex::Regex; use regex::Regex;
use std::borrow::Cow; use std::borrow::Cow;
use std::time::Duration; use std::time::Duration;
@ -27,7 +28,7 @@ use crate::{
CONFIG, CONFIG,
}; };
static AC_CACHE: Lazy<Cache<String, AuthenticatedUser>> = static AC_CACHE: Lazy<Cache<OIDCState, AuthenticatedUser>> =
Lazy::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build()); Lazy::new(|| Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(10 * 60)).build());
static CLIENT_CACHE_KEY: Lazy<String> = Lazy::new(|| "sso-client".to_string()); static CLIENT_CACHE_KEY: Lazy<String> = Lazy::new(|| "sso-client".to_string());
@ -54,6 +55,46 @@ impl<'a, AD: AuthDisplay, P: AuthPrompt, RT: ResponseType> AuthorizationRequestE
} }
} }
#[derive(
Clone,
Debug,
Default,
DieselNewType,
FromForm,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Deref,
Display,
From,
)]
#[deref(forward)]
#[from(forward)]
pub struct OIDCCode(String);
#[derive(
Clone,
Debug,
Default,
DieselNewType,
FromForm,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Deref,
Display,
From,
)]
#[deref(forward)]
#[from(forward)]
pub struct OIDCState(String);
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct SsoTokenJwtClaims { struct SsoTokenJwtClaims {
// Not before // Not before
@ -81,11 +122,11 @@ pub fn encode_ssotoken_claims() -> String {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum OIDCCodeWrapper { pub enum OIDCCodeWrapper {
Ok { Ok {
state: String, state: OIDCState,
code: String, code: OIDCCode,
}, },
Error { Error {
state: String, state: OIDCState,
error: String, error: String,
error_description: Option<String>, error_description: Option<String>,
}, },
@ -208,12 +249,29 @@ impl CoreClientExt for CoreClient {
} }
} }
pub fn deocde_state(base64_state: String) -> ApiResult<OIDCState> {
let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) {
Ok(vec) => match String::from_utf8(vec) {
Ok(valid) => OIDCState(valid),
Err(_) => err!(format!("Invalid utf8 chars in {base64_state} after base64 decoding")),
},
Err(_) => err!(format!("Failed to decode {base64_state} using base64")),
};
Ok(state)
}
// The `nonce` allow to protect against replay attacks // The `nonce` allow to protect against replay attacks
// The `state` is encoded using base64 to ensure no issue with providers (It contains the Organization identifier). // The `state` is encoded using base64 to ensure no issue with providers (It contains the Organization identifier).
// redirect_uri from: https://github.com/bitwarden/server/blob/main/src/Identity/IdentityServer/ApiClient.cs // redirect_uri from: https://github.com/bitwarden/server/blob/main/src/Identity/IdentityServer/ApiClient.cs
pub async fn authorize_url(state: String, client_id: &str, raw_redirect_uri: &str, mut conn: DbConn) -> ApiResult<Url> { pub async fn authorize_url(
state: OIDCState,
client_id: &str,
raw_redirect_uri: &str,
mut conn: DbConn,
) -> ApiResult<Url> {
let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new); let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new);
let base64_state = data_encoding::BASE64.encode(state.as_bytes()); let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes());
let redirect_uri = match client_id { let redirect_uri = match client_id {
"web" | "browser" => format!("{}/sso-connector.html", CONFIG.domain()), "web" | "browser" => format!("{}/sso-connector.html", CONFIG.domain()),
@ -254,12 +312,38 @@ pub async fn authorize_url(state: String, client_id: &str, raw_redirect_uri: &st
Ok(auth_url) Ok(auth_url)
} }
#[derive(
Clone,
Debug,
Default,
DieselNewType,
FromForm,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Deref,
Display,
From,
)]
#[deref(forward)]
#[from(forward)]
pub struct OIDCIdentifier(String);
impl OIDCIdentifier {
fn new(issuer: &str, subject: &str) -> Self {
OIDCIdentifier(format!("{}/{}", issuer, subject))
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AuthenticatedUser { pub struct AuthenticatedUser {
pub refresh_token: Option<String>, pub refresh_token: Option<String>,
pub access_token: String, pub access_token: String,
pub expires_in: Option<Duration>, pub expires_in: Option<Duration>,
pub identifier: String, pub identifier: OIDCIdentifier,
pub email: String, pub email: String,
pub email_verified: Option<bool>, pub email_verified: Option<bool>,
pub user_name: Option<String>, pub user_name: Option<String>,
@ -267,14 +351,14 @@ pub struct AuthenticatedUser {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct UserInformation { pub struct UserInformation {
pub state: String, pub state: OIDCState,
pub identifier: String, pub identifier: OIDCIdentifier,
pub email: String, pub email: String,
pub email_verified: Option<bool>, pub email_verified: Option<bool>,
pub user_name: Option<String>, pub user_name: Option<String>,
} }
async fn decode_code_claims(code: &str, conn: &mut DbConn) -> ApiResult<(String, String)> { async fn decode_code_claims(code: &str, conn: &mut DbConn) -> ApiResult<(OIDCCode, OIDCState)> {
match auth::decode_jwt::<OIDCCodeClaims>(code, SSO_JWT_ISSUER.to_string()) { match auth::decode_jwt::<OIDCCodeClaims>(code, SSO_JWT_ISSUER.to_string()) {
Ok(code_claims) => match code_claims.code { Ok(code_claims) => match code_claims.code {
OIDCCodeWrapper::Ok { OIDCCodeWrapper::Ok {
@ -317,7 +401,7 @@ pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<U
}); });
} }
let oidc_code = AuthorizationCode::new(code.clone()); let oidc_code = AuthorizationCode::new(code.to_string());
let client = CoreClient::cached().await?; let client = CoreClient::cached().await?;
let nonce = match SsoNonce::find(&state, conn).await { let nonce = match SsoNonce::find(&state, conn).await {
@ -377,7 +461,7 @@ pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<U
error!("Scope offline_access is present but response contain no refresh_token"); error!("Scope offline_access is present but response contain no refresh_token");
} }
let identifier = format!("{}/{}", **id_claims.issuer(), **id_claims.subject()); let identifier = OIDCIdentifier::new(id_claims.issuer(), id_claims.subject());
let authenticated_user = AuthenticatedUser { let authenticated_user = AuthenticatedUser {
refresh_token, refresh_token,
@ -404,7 +488,7 @@ pub async fn exchange_code(wrapped_code: &str, conn: &mut DbConn) -> ApiResult<U
} }
// User has passed 2FA flow we can delete `nonce` and clear the cache. // User has passed 2FA flow we can delete `nonce` and clear the cache.
pub async fn redeem(state: &String, conn: &mut DbConn) -> ApiResult<AuthenticatedUser> { pub async fn redeem(state: &OIDCState, conn: &mut DbConn) -> ApiResult<AuthenticatedUser> {
if let Err(err) = SsoNonce::delete(state, conn).await { if let Err(err) = SsoNonce::delete(state, conn).await {
error!("Failed to delete database sso_nonce using {state}: {err}") error!("Failed to delete database sso_nonce using {state}: {err}")
} }