api: tfa management and login

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller
2020-11-16 14:37:22 +01:00
parent dc1fdd6267
commit 027ef213aa
7 changed files with 1585 additions and 220 deletions

View File

@ -600,8 +600,9 @@ fn check_auth(
let ticket = user_auth_data.ticket.clone();
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
let userid: Userid = Ticket::parse(&ticket)?
.verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?;
let userid: Userid = Ticket::<super::ticket::ApiTicket>::parse(&ticket)?
.verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?
.require_full()?;
let auth_id = Authid::from(userid.clone());
if !user_info.is_active_auth_id(&auth_id) {

77
src/server/ticket.rs Normal file
View File

@ -0,0 +1,77 @@
use std::fmt;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use crate::api2::types::Userid;
use crate::config::tfa;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PartialTicket {
#[serde(rename = "u")]
userid: Userid,
#[serde(rename = "c")]
challenge: tfa::TfaChallenge,
}
/// A new ticket struct used in rest.rs's `check_auth` - mostly for better errors than failing to
/// parse the userid ticket content.
pub enum ApiTicket {
Full(Userid),
Partial(tfa::TfaChallenge),
}
impl ApiTicket {
/// Require the ticket to be a full ticket, otherwise error with a meaningful error message.
pub fn require_full(self) -> Result<Userid, Error> {
match self {
ApiTicket::Full(userid) => Ok(userid),
ApiTicket::Partial(_) => bail!("access denied - second login factor required"),
}
}
/// Expect the ticket to contain a tfa challenge, otherwise error with a meaningful error
/// message.
pub fn require_partial(self) -> Result<tfa::TfaChallenge, Error> {
match self {
ApiTicket::Full(_) => bail!("invalid tfa challenge"),
ApiTicket::Partial(challenge) => Ok(challenge),
}
}
/// Create a new full ticket.
pub fn full(userid: Userid) -> Self {
ApiTicket::Full(userid)
}
/// Create a new partial ticket.
pub fn partial(challenge: tfa::TfaChallenge) -> Self {
ApiTicket::Partial(challenge)
}
}
impl fmt::Display for ApiTicket {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ApiTicket::Full(userid) => fmt::Display::fmt(userid, f),
ApiTicket::Partial(partial) => {
let data = serde_json::to_string(partial).map_err(|_| fmt::Error)?;
write!(f, "!tfa!{}", data)
}
}
}
}
impl std::str::FromStr for ApiTicket {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.starts_with("!tfa!") {
Ok(ApiTicket::Partial(serde_json::from_str(&s[5..])?))
} else {
Ok(ApiTicket::Full(s.parse()?))
}
}
}