Skip to content

Commit

Permalink
Allow SSO role mapping to add admin cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
Timshel committed Jan 18, 2024
1 parent 273363b commit 1eb8390
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 57 deletions.
6 changes: 6 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,12 @@
# SSO_KEY_FILEPATH=%DATA_FOLDER%/sso_key.pub.pem
## Optional Master password policy (minComplexity=[0-4])
# SSO_MASTER_PASSWORD_POLICY='{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":false}'
## Enable the mapping of roles (user/admin) from the access_token
# SSO_ROLES_ENABLED=false
## Missing/Invalid roles default to user
# SSO_ROLES_DEFAULT_TO_USER=true
## Access token path to read roles
# SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles

## Set the lifetime of admin sessions to this value (in minutes).
# ADMIN_SESSION_LIFETIME=20
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ Based on [Timshel/sso-support](https://github.com/Timshel/vaultwarden/tree/sso-s

:warning: Branch will be rebased and forced-pushed from time to time. :warning:

## Additionnal features

This branch now contain additionnal features not added to the SSO [PR](https://github.com/dani-garcia/vaultwarden/pull/3899) since it would slow even more it's review.

### Role mapping

Allow to map roles from the Access token to users to grant access to `VaultWarden` `admin` console.
Support two roles: `admin` or `user`.

This feature is controlled by the following conf:

- `SSO_ROLES_ENABLED`: control if the mapping is done, default is `false`
- `SSO_ROLES_DEFAULT_TO_USER`: do not block login in case of missing or invalid roles, default is `true`.
- `SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles`: path to read roles in the Access token

## Docker

Change the docker files to package both front-end from [Timshel/oidc_web_builds](https://github.com/Timshel/oidc_web_builds/releases).
Expand Down
3 changes: 3 additions & 0 deletions SSO.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ The following configurations are available
- `SSO_AUTH_FAILURE_SILENT`: Silently redirect to the home instead of displaying a JSON error.
- `SSO_KEY_FILEPATH` : Optional public key to validate the JWT token (without it signature check will not be done).
- `SSO_MASTER_PASSWORD_POLICY`: Optional Master password policy
- `SSO_ROLES_ENABLED`: control if the mapping is done, default is `false`
- `SSO_ROLES_DEFAULT_TO_USER`: do not block login in case of missing or invalid roles, default is `true`.
- `SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles`: path to read roles in the Access token

The callback url is : `https://your.domain/identity/connect/oidc-signin`

Expand Down
34 changes: 21 additions & 13 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ use crate::{
CONFIG, VERSION,
};

#[allow(clippy::nonminimal_bool)]
pub fn routes() -> Vec<Route> {
if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {
if !CONFIG.disable_admin_token()
&& !CONFIG.is_admin_token_set()
&& !(CONFIG.sso_enabled() && CONFIG.sso_roles_enabled())
{
return routes![admin_disabled];
}

Expand Down Expand Up @@ -88,15 +92,15 @@ fn admin_disabled() -> &'static str {
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
}

const COOKIE_NAME: &str = "VW_ADMIN";
pub const COOKIE_NAME: &str = "VW_ADMIN";
const ADMIN_PATH: &str = "/admin";
const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";

const BASE_TEMPLATE: &str = "admin/base";

const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";

fn admin_path() -> String {
pub fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
}

Expand Down Expand Up @@ -151,6 +155,7 @@ fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<
let json = json!({
"page_content": "admin/login",
"error": msg,
"sso_only": CONFIG.sso_enabled() && CONFIG.sso_roles_enabled(),
"redirect": redirect,
"urlpath": CONFIG.domain_path()
});
Expand All @@ -166,6 +171,18 @@ struct LoginForm {
redirect: Option<String>,
}

pub fn create_admin_cookie<'a>() -> Cookie<'a> {
let claims = generate_admin_claims();
let jwt = encode_jwt(&claims);

Cookie::build((COOKIE_NAME, jwt))
.path(admin_path())
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime()))
.same_site(SameSite::Strict)
.http_only(true)
.into()
}

#[post("/", data = "<data>")]
fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> {
let data = data.into_inner();
Expand All @@ -184,16 +201,7 @@ fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp
Err(AdminResponse::Unauthorized(render_admin_login(Some("Invalid admin token, please try again."), redirect)))
} else {
// If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims();
let jwt = encode_jwt(&claims);

let cookie = Cookie::build((COOKIE_NAME, jwt))
.path(admin_path())
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime()))
.same_site(SameSite::Strict)
.http_only(true);

cookies.add(cookie);
cookies.add(create_admin_cookie());
if let Some(redirect) = redirect {
Ok(Redirect::to(format!("{}{}", admin_path(), redirect)))
} else {
Expand Down
27 changes: 20 additions & 7 deletions src/api/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use serde_json::Value;

use crate::{
api::{
admin,
core::{
accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade},
log_user_event,
Expand All @@ -31,7 +32,12 @@ pub fn routes() -> Vec<Route> {
}

#[post("/connect/token", data = "<data>")]
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
async fn login(
data: Form<ConnectData>,
client_header: ClientHeaders,
cookies: &CookieJar<'_>,
mut conn: DbConn,
) -> JsonResult {
let data: ConnectData = data.into_inner();

let mut user_uuid: Option<String> = None;
Expand Down Expand Up @@ -72,7 +78,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;

_sso_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
_sso_login(data, &mut user_uuid, &mut conn, cookies, &client_header.ip).await
}
t => err!("Invalid type", t),
};
Expand Down Expand Up @@ -150,11 +156,13 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
}

// After exchanging the code we need to check first if 2FA is needed before continuing
async fn _sso_login(data: ConnectData, user_uuid: &mut Option<String>, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
if !CONFIG.sso_enabled() {
err!("SSO sign-in is disabled");
}

async fn _sso_login(
data: ConnectData,
user_uuid: &mut Option<String>,
conn: &mut DbConn,
cookies: &CookieJar<'_>,
ip: &ClientIp,
) -> JsonResult {
AuthMethod::Sso.check_scope(data.scope.as_ref())?;

// Ratelimit the login
Expand Down Expand Up @@ -198,6 +206,11 @@ async fn _sso_login(data: ConnectData, user_uuid: &mut Option<String>, conn: &mu
// Set the user_uuid here to be passed back used for event logging.
*user_uuid = Some(user.uuid.clone());

if auth_user.is_admin() {
info!("User {} logged with admin cookie", user.email);
cookies.add(admin::create_admin_cookie());
}

let auth_tokens = sso::create_auth_tokens(&device, &user, auth_user.refresh_token, &auth_user.access_token)?;

authenticated_response(&user, &mut device, new_device, auth_tokens, twofactor_token, &now, conn, ip).await
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,12 @@ make_config! {
sso_key_filepath: String, false, auto, |c| format!("{}/{}", c.data_folder, "sso_key.pub.pem");
/// Optional sso master password policy
sso_master_password_policy: String, false, option;
/// Enable the mapping of roles (user/admin) from the access_token
sso_roles_enabled: bool, true, def, false;
/// Missing invalid roles default to user
sso_roles_default_to_user: bool, true, def, true;
/// Access token path to read roles
sso_roles_token_path: String, false, auto, |c| format!("/resource_access/{}/roles", c.sso_client_id);
},

/// Yubikey settings
Expand Down
Loading

0 comments on commit 1eb8390

Please sign in to comment.