From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pdm-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id DB7B81FF168 for <inbox@lore.proxmox.com>; Tue, 4 Mar 2025 13:05:56 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8590D1E017; Tue, 4 Mar 2025 13:05:53 +0100 (CET) From: Shannon Sterz <s.sterz@proxmox.com> To: pdm-devel@lists.proxmox.com Date: Tue, 4 Mar 2025 13:04:57 +0100 Message-Id: <20250304120506.135617-13-s.sterz@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250304120506.135617-1-s.sterz@proxmox.com> References: <20250304120506.135617-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.018 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH proxmox v4 12/21] login: add helpers to pass cookie values when parsing login responses X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion <pdm-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pdm-devel/> List-Post: <mailto:pdm-devel@lists.proxmox.com> List-Help: <mailto:pdm-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Datacenter Manager development discussion <pdm-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" <pdm-devel-bounces@lists.proxmox.com> depending on the context a client may or may not have access to HttpOnly cookies. this change allows them to pass such values to `proxmox-login` to take them into account when parsing login responses. Signed-off-by: Shannon Sterz <s.sterz@proxmox.com> --- proxmox-login/src/lib.rs | 89 +++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/proxmox-login/src/lib.rs b/proxmox-login/src/lib.rs index 3b3558d0..52282052 100644 --- a/proxmox-login/src/lib.rs +++ b/proxmox-login/src/lib.rs @@ -162,10 +162,27 @@ impl Login { &self, body: &T, ) -> Result<TicketResult, ResponseError> { - self.response_bytes(body.as_ref()) + self.response_bytes(None, body.as_ref()) } - fn response_bytes(&self, body: &[u8]) -> Result<TicketResult, ResponseError> { + /// Parse the result body of a [`CreateTicket`](api::CreateTicket) API request taking into + /// account potential tickets obtained via a `Set-Cookie` header. + /// + /// On success, this will either yield an [`Authentication`] or a [`SecondFactorChallenge`] if + /// Two-Factor-Authentication is required. + pub fn response_with_cookie_ticket<T: ?Sized + AsRef<[u8]>>( + &self, + cookie_ticket: Option<Ticket>, + body: &T, + ) -> Result<TicketResult, ResponseError> { + self.response_bytes(cookie_ticket, body.as_ref()) + } + + fn response_bytes( + &self, + cookie_ticket: Option<Ticket>, + body: &[u8], + ) -> Result<TicketResult, ResponseError> { use ticket::TicketResponse; let response: api::ApiResponse<api::CreateTicketResponse> = serde_json::from_slice(body)?; @@ -175,6 +192,14 @@ impl Login { return Err("ticket response contained unexpected userid".into()); } + // if a ticket was provided via a cookie, use it like a normal ticket + if let Some(ticket) = cookie_ticket { + check_ticket_userid(ticket.userid(), &self.userid)?; + return Ok(TicketResult::Full( + self.authentication_for(ticket, response)?, + )); + } + let ticket: TicketResponse = match response.ticket { Some(ticket) => ticket.parse()?, None => return Err("missing ticket".into()), @@ -183,15 +208,7 @@ impl Login { Ok(match ticket { TicketResponse::Full(ticket) => { check_ticket_userid(ticket.userid(), &self.userid)?; - TicketResult::Full(Authentication { - csrfprevention_token: response - .csrfprevention_token - .ok_or("missing CSRFPreventionToken in ticket response")?, - clustername: response.clustername, - api_url: self.api_url.clone(), - userid: response.username, - ticket, - }) + TicketResult::Full(self.authentication_for(ticket, response)?) } TicketResponse::Tfa(ticket, challenge) => { @@ -205,6 +222,22 @@ impl Login { } }) } + + fn authentication_for( + &self, + ticket: Ticket, + response: api::CreateTicketResponse, + ) -> Result<Authentication, ResponseError> { + Ok(Authentication { + csrfprevention_token: response + .csrfprevention_token + .ok_or("missing CSRFPreventionToken in ticket response")?, + clustername: response.clustername, + api_url: self.api_url.clone(), + userid: response.username, + ticket, + }) + } } /// This is the result of a ticket call. It will either yield a final ticket, or a TFA challenge. @@ -310,10 +343,24 @@ impl SecondFactorChallenge { &self, body: &T, ) -> Result<Authentication, ResponseError> { - self.response_bytes(body.as_ref()) + self.response_bytes(None, body.as_ref()) } - fn response_bytes(&self, body: &[u8]) -> Result<Authentication, ResponseError> { + /// Deal with the API's response object to extract the ticket either from a cookie or the + /// response itself. + pub fn response_with_cookie_ticket<T: ?Sized + AsRef<[u8]>>( + &self, + cookie_ticket: Option<Ticket>, + body: &T, + ) -> Result<Authentication, ResponseError> { + self.response_bytes(cookie_ticket, body.as_ref()) + } + + fn response_bytes( + &self, + cookie_ticket: Option<Ticket>, + body: &[u8], + ) -> Result<Authentication, ResponseError> { let response: api::ApiResponse<api::CreateTicketResponse> = serde_json::from_slice(body)?; let response = response.data.ok_or("missing response data")?; @@ -321,7 +368,21 @@ impl SecondFactorChallenge { return Err("ticket response contained unexpected userid".into()); } - let ticket: Ticket = response.ticket.ok_or("no ticket in response")?.parse()?; + // get the ticket from: + // 1. the cookie if possible -> new HttpOnly authentication outside of the browser + // 2. just the `ticket_info` -> new HttpOnly authentication inside a browser context or + // similar, assume the ticket is handle by that + // 3. the `ticket` field -> old authentication flow where we handle the ticket ourselves + let ticket: Ticket = cookie_ticket + .ok_or(ResponseError::from("no ticket in response")) + .or_else(|e| { + response + .ticket_info + .or(response.ticket) + .ok_or(e) + .and_then(|t| t.parse().map_err(|e: TicketError| e.into())) + })?; + check_ticket_userid(ticket.userid(), &self.userid)?; Ok(Authentication { -- 2.39.5 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel