2021-06-14 09:58:28 +00:00
|
|
|
//! OpenID redirect/login API
|
|
|
|
use std::convert::TryFrom;
|
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
use anyhow::{bail, format_err, Error};
|
2021-06-14 09:58:28 +00:00
|
|
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
|
|
|
use proxmox::api::router::{Router, SubdirMap};
|
|
|
|
use proxmox::api::{api, Permission, RpcEnvironment};
|
2021-09-21 05:58:50 +00:00
|
|
|
use proxmox::{http_err, list_subdirs_api_method, identity, sortable};
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-07-01 12:58:32 +00:00
|
|
|
use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig};
|
|
|
|
|
2021-09-09 11:14:28 +00:00
|
|
|
use pbs_api_types::{Userid, User, REALM_ID_SCHEMA};
|
2021-07-09 11:01:54 +00:00
|
|
|
use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
|
2021-07-12 09:07:52 +00:00
|
|
|
use pbs_tools::ticket::Ticket;
|
2021-09-02 10:47:11 +00:00
|
|
|
use pbs_config::domains::{OpenIdUserAttribute, OpenIdRealmConfig};
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-10 04:53:53 +00:00
|
|
|
use pbs_config::CachedUserInfo;
|
2021-09-02 10:47:11 +00:00
|
|
|
use pbs_config::open_backup_lockfile;
|
2021-07-20 11:51:54 +00:00
|
|
|
|
2021-06-14 09:58:28 +00:00
|
|
|
use crate::auth_helpers::*;
|
2021-09-29 09:05:26 +00:00
|
|
|
use crate::server::ticket::ApiTicket;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-07-01 12:58:32 +00:00
|
|
|
fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
|
|
|
|
let config = OpenIdConfig {
|
|
|
|
issuer_url: realm_config.issuer_url.clone(),
|
|
|
|
client_id: realm_config.client_id.clone(),
|
|
|
|
client_key: realm_config.client_key.clone(),
|
|
|
|
};
|
|
|
|
OpenIdAuthenticator::discover(&config, redirect_url)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-14 09:58:28 +00:00
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
state: {
|
|
|
|
description: "OpenId state.",
|
|
|
|
type: String,
|
|
|
|
},
|
|
|
|
code: {
|
|
|
|
description: "OpenId authorization code.",
|
|
|
|
type: String,
|
|
|
|
},
|
|
|
|
"redirect-url": {
|
|
|
|
description: "Redirection Url. The client should set this to used server url.",
|
|
|
|
type: String,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
returns: {
|
|
|
|
properties: {
|
|
|
|
username: {
|
|
|
|
type: String,
|
|
|
|
description: "User name.",
|
|
|
|
},
|
|
|
|
ticket: {
|
|
|
|
type: String,
|
|
|
|
description: "Auth ticket.",
|
|
|
|
},
|
|
|
|
CSRFPreventionToken: {
|
|
|
|
type: String,
|
|
|
|
description: "Cross Site Request Forgery Prevention Token.",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
protected: true,
|
|
|
|
access: {
|
|
|
|
permission: &Permission::World,
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Verify OpenID authorization code and create a ticket
|
|
|
|
///
|
|
|
|
/// Returns: An authentication ticket with additional infos.
|
|
|
|
pub fn openid_login(
|
|
|
|
state: String,
|
|
|
|
code: String,
|
|
|
|
redirect_url: String,
|
2021-09-21 05:58:50 +00:00
|
|
|
rpcenv: &mut dyn RpcEnvironment,
|
2021-06-14 09:58:28 +00:00
|
|
|
) -> Result<Value, Error> {
|
2021-09-21 05:58:50 +00:00
|
|
|
use proxmox_rest_server::RestEnvironment;
|
|
|
|
|
|
|
|
let env: &RestEnvironment = rpcenv.as_any().downcast_ref::<RestEnvironment>()
|
|
|
|
.ok_or_else(|| format_err!("detected worng RpcEnvironment type"))?;
|
|
|
|
|
2021-06-14 09:58:28 +00:00
|
|
|
let user_info = CachedUserInfo::new()?;
|
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let mut tested_username = None;
|
|
|
|
|
|
|
|
let result = proxmox::try_block!({
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let (realm, private_auth_state) =
|
|
|
|
OpenIdAuthenticator::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M!(), &state)?;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let (domains, _digest) = pbs_config::domains::config()?;
|
|
|
|
let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let open_id = openid_authenticator(&config, &redirect_url)?;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let info = open_id.verify_authorization_code(&code, &private_auth_state)?;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
// eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
|
|
|
|
|
|
|
|
let unique_name = match config.username_claim {
|
|
|
|
None | Some(OpenIdUserAttribute::Subject) => info.subject().as_str(),
|
|
|
|
Some(OpenIdUserAttribute::Username) => {
|
|
|
|
match info.preferred_username() {
|
|
|
|
Some(name) => name.as_str(),
|
|
|
|
None => bail!("missing claim 'preferred_name'"),
|
|
|
|
}
|
2021-06-14 09:58:28 +00:00
|
|
|
}
|
2021-09-21 05:58:50 +00:00
|
|
|
Some(OpenIdUserAttribute::Email) => {
|
|
|
|
match info.email() {
|
|
|
|
Some(name) => name.as_str(),
|
|
|
|
None => bail!("missing claim 'email'"),
|
|
|
|
}
|
2021-06-14 09:58:28 +00:00
|
|
|
}
|
2021-09-21 05:58:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
|
|
|
|
tested_username = Some(unique_name.to_string());
|
|
|
|
|
|
|
|
if !user_info.is_active_user_id(&user_id) {
|
|
|
|
if config.autocreate.unwrap_or(false) {
|
|
|
|
use pbs_config::user;
|
|
|
|
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
|
|
|
|
let user = User {
|
|
|
|
userid: user_id.clone(),
|
|
|
|
comment: None,
|
|
|
|
enable: None,
|
|
|
|
expire: None,
|
|
|
|
firstname: info.given_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
|
|
|
|
lastname: info.family_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
|
|
|
|
email: info.email().map(|e| e.to_string()),
|
|
|
|
};
|
|
|
|
let (mut config, _digest) = user::config()?;
|
|
|
|
if config.sections.get(user.userid.as_str()).is_some() {
|
|
|
|
bail!("autocreate user failed - '{}' already exists.", user.userid);
|
|
|
|
}
|
|
|
|
config.set_data(user.userid.as_str(), "user", &user)?;
|
|
|
|
user::save_config(&config)?;
|
|
|
|
} else {
|
|
|
|
bail!("user account '{}' missing, disabled or expired.", user_id);
|
2021-06-14 09:58:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
let api_ticket = ApiTicket::full(user_id.clone());
|
|
|
|
let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
|
|
|
|
let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
env.log_auth(user_id.as_str());
|
|
|
|
|
|
|
|
Ok(json!({
|
|
|
|
"username": user_id,
|
|
|
|
"ticket": ticket,
|
|
|
|
"CSRFPreventionToken": token,
|
|
|
|
}))
|
|
|
|
});
|
|
|
|
|
|
|
|
if let Err(ref err) = result {
|
|
|
|
let msg = err.to_string();
|
|
|
|
env.log_failed_auth(tested_username, &msg);
|
|
|
|
return Err(http_err!(UNAUTHORIZED, "{}", msg))
|
|
|
|
}
|
2021-06-14 09:58:28 +00:00
|
|
|
|
2021-09-21 05:58:50 +00:00
|
|
|
result
|
2021-06-14 09:58:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
protected: true,
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
realm: {
|
|
|
|
schema: REALM_ID_SCHEMA,
|
|
|
|
},
|
|
|
|
"redirect-url": {
|
|
|
|
description: "Redirection Url. The client should set this to used server url.",
|
|
|
|
type: String,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
returns: {
|
|
|
|
description: "Redirection URL.",
|
|
|
|
type: String,
|
|
|
|
},
|
|
|
|
access: {
|
|
|
|
description: "Anyone can access this (before the user is authenticated).",
|
|
|
|
permission: &Permission::World,
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Create OpenID Redirect Session
|
|
|
|
fn openid_auth_url(
|
|
|
|
realm: String,
|
|
|
|
redirect_url: String,
|
|
|
|
_rpcenv: &mut dyn RpcEnvironment,
|
|
|
|
) -> Result<String, Error> {
|
|
|
|
|
2021-09-02 10:47:11 +00:00
|
|
|
let (domains, _digest) = pbs_config::domains::config()?;
|
2021-06-14 09:58:28 +00:00
|
|
|
let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
|
|
|
|
|
2021-07-01 12:58:32 +00:00
|
|
|
let open_id = openid_authenticator(&config, &redirect_url)?;
|
2021-06-14 09:58:28 +00:00
|
|
|
|
|
|
|
let url = open_id.authorize_url(PROXMOX_BACKUP_RUN_DIR_M!(), &realm)?
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
Ok(url.into())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[sortable]
|
|
|
|
const SUBDIRS: SubdirMap = &sorted!([
|
|
|
|
("login", &Router::new().post(&API_METHOD_OPENID_LOGIN)),
|
|
|
|
("auth-url", &Router::new().post(&API_METHOD_OPENID_AUTH_URL)),
|
|
|
|
]);
|
|
|
|
|
|
|
|
pub const ROUTER: Router = Router::new()
|
|
|
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
|
|
|
.subdirs(SUBDIRS);
|