Merge branch 'proxmox-openid-merge'
This commit is contained in:
commit
6253f263ce
29
proxmox-openid/Cargo.toml
Normal file
29
proxmox-openid/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "proxmox-openid"
|
||||||
|
version = "0.9.9"
|
||||||
|
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "AGPL-3"
|
||||||
|
exclude = [
|
||||||
|
"build",
|
||||||
|
"debian",
|
||||||
|
]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "proxmox_openid"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
http = "0.2"
|
||||||
|
nix = "0.26"
|
||||||
|
openidconnect = { version = "2.4", default-features = false, features = ["accept-rfc3339-timestamps"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror="1.0"
|
||||||
|
ureq = { version = "2.4", default-features = false, features = ["native-tls", "gzip"] }
|
||||||
|
native-tls = "0.2"
|
||||||
|
url = "2.1"
|
||||||
|
|
||||||
|
proxmox-time = "1"
|
||||||
|
proxmox-sys = { version = "0.4", features = ["timer"] }
|
141
proxmox-openid/debian/changelog
Normal file
141
proxmox-openid/debian/changelog
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
rust-proxmox-openid (0.9.9-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* update openidconnect to 2.4
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 11 Jan 2023 18:41:25 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.8-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* update nix to 0.26
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 05 Jan 2023 12:25:10 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.7-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* bump proxmox-sys to 0.4
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 28 Jul 2022 13:40:44 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.6-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* rebuild with nix 0.24 and proxmox-sys 0.3
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 2 Jun 2022 12:38:28 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.5-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* avoid chunked transfer-encoding when submitting to the provider's token
|
||||||
|
endpoint, as some providers like Microsoft's Azure are quite inflexible
|
||||||
|
and cannot cope with such basic HTTP requests.
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Fri, 01 Apr 2022 15:56:07 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.4-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* re-add HTTP proxy support via the ALL_PROXY environment variable. This got
|
||||||
|
lost with switching the HTTP client from curl to ureq.
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 22 Mar 2022 11:31:08 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.3-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* use much simpler ureq (with native-tls) HTTP client instead of curl
|
||||||
|
|
||||||
|
* enable "accept-rfc3339-timestamps" feature to fix support for some OIDC
|
||||||
|
providers like `auth0`
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 01 Feb 2022 09:08:31 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.2-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* depend on proxmox-sys 0.2
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 23 Nov 2021 12:35:41 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* rebuild with openidconnect 0.2.1
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 12:54:24 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.9.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* allow to configure used scopes
|
||||||
|
|
||||||
|
* allow to configure prompt behaviour
|
||||||
|
|
||||||
|
* allow to configure acr values
|
||||||
|
|
||||||
|
* new helper verify_authorization_code_simple()
|
||||||
|
|
||||||
|
* also return data from UserInfo endpoint
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 09:36:29 +0100
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.8.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* add fsync parameter to replace_file
|
||||||
|
|
||||||
|
* Depend on proxmox 0.15.0
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 21 Oct 2021 07:14:52 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.8.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* update to proxmox crate split
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Fri, 08 Oct 2021 12:19:55 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.7.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* bump proxmox to 0.13.0
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 24 Aug 2021 16:06:55 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.6.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* depend on proxmox 0.12.0
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 20 Jul 2021 13:19:23 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.6.0-2) unstable; urgency=medium
|
||||||
|
|
||||||
|
* remove debug output
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 30 Jun 2021 08:43:06 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.6.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* use one lock file per realm
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Fri, 25 Jun 2021 11:09:08 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.5.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* avoid unused features "sortable-macro" and "api-macro"
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Jun 2021 11:29:05 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.4.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* set "default-features = false" for proxmox crate
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Jun 2021 11:17:22 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.3.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* return authorize_url() as string
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 22 Jun 2021 09:23:33 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.2.0-1) devel; urgency=medium
|
||||||
|
|
||||||
|
* implement Deserialize/Serialize for OpenIdConfig
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Mon, 21 Jun 2021 13:37:24 +0200
|
||||||
|
|
||||||
|
rust-proxmox-openid (0.1.0-1) devel; urgency=medium
|
||||||
|
|
||||||
|
* initial release
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Fri, 18 Jun 2021 16:05:49 +0200
|
61
proxmox-openid/debian/control
Normal file
61
proxmox-openid/debian/control
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
Source: rust-proxmox-openid
|
||||||
|
Section: rust
|
||||||
|
Priority: optional
|
||||||
|
Build-Depends: debhelper (>= 12),
|
||||||
|
dh-cargo (>= 25),
|
||||||
|
cargo:native <!nocheck>,
|
||||||
|
rustc:native <!nocheck>,
|
||||||
|
libstd-rust-dev <!nocheck>,
|
||||||
|
librust-anyhow-1+default-dev <!nocheck>,
|
||||||
|
librust-http-0.2+default-dev <!nocheck>,
|
||||||
|
librust-native-tls-0.2+default-dev <!nocheck>,
|
||||||
|
librust-nix-0.26+default-dev <!nocheck>,
|
||||||
|
librust-openidconnect-2+accept-rfc3339-timestamps-dev (>= 2.4-~~) <!nocheck>,
|
||||||
|
librust-proxmox-sys-0.4+default-dev <!nocheck>,
|
||||||
|
librust-proxmox-sys-0.4+timer-dev <!nocheck>,
|
||||||
|
librust-proxmox-time-1+default-dev <!nocheck>,
|
||||||
|
librust-serde-1+default-dev <!nocheck>,
|
||||||
|
librust-serde-1+derive-dev <!nocheck>,
|
||||||
|
librust-serde-json-1+default-dev <!nocheck>,
|
||||||
|
librust-thiserror-1+default-dev <!nocheck>,
|
||||||
|
librust-ureq-2+gzip-dev (>= 2.4-~~) <!nocheck>,
|
||||||
|
librust-ureq-2+native-tls-dev (>= 2.4-~~) <!nocheck>,
|
||||||
|
librust-url-2+default-dev (>= 2.1-~~) <!nocheck>
|
||||||
|
Maintainer: Proxmox Support Team <support@proxmox.com>
|
||||||
|
Standards-Version: 4.6.1
|
||||||
|
Vcs-Git:
|
||||||
|
Vcs-Browser:
|
||||||
|
X-Cargo-Crate: proxmox-openid
|
||||||
|
Rules-Requires-Root: no
|
||||||
|
|
||||||
|
Package: librust-proxmox-openid-dev
|
||||||
|
Architecture: any
|
||||||
|
Multi-Arch: same
|
||||||
|
Depends:
|
||||||
|
${misc:Depends},
|
||||||
|
librust-anyhow-1+default-dev,
|
||||||
|
librust-http-0.2+default-dev,
|
||||||
|
librust-native-tls-0.2+default-dev,
|
||||||
|
librust-nix-0.26+default-dev,
|
||||||
|
librust-openidconnect-2+accept-rfc3339-timestamps-dev (>= 2.4-~~),
|
||||||
|
librust-proxmox-sys-0.4+default-dev,
|
||||||
|
librust-proxmox-sys-0.4+timer-dev,
|
||||||
|
librust-proxmox-time-1+default-dev,
|
||||||
|
librust-serde-1+default-dev,
|
||||||
|
librust-serde-1+derive-dev,
|
||||||
|
librust-serde-json-1+default-dev,
|
||||||
|
librust-thiserror-1+default-dev,
|
||||||
|
librust-ureq-2+gzip-dev (>= 2.4-~~),
|
||||||
|
librust-ureq-2+native-tls-dev (>= 2.4-~~),
|
||||||
|
librust-url-2+default-dev (>= 2.1-~~)
|
||||||
|
Provides:
|
||||||
|
librust-proxmox-openid+default-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0+default-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0.9-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0.9+default-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0.9.9-dev (= ${binary:Version}),
|
||||||
|
librust-proxmox-openid-0.9.9+default-dev (= ${binary:Version})
|
||||||
|
Description: Rust crate "proxmox-openid" - Rust source code
|
||||||
|
This package contains the source for the Rust proxmox-openid crate, packaged by
|
||||||
|
debcargo for use with cargo and dh-cargo.
|
16
proxmox-openid/debian/copyright
Normal file
16
proxmox-openid/debian/copyright
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
|
||||||
|
|
||||||
|
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
8
proxmox-openid/debian/debcargo.toml
Normal file
8
proxmox-openid/debian/debcargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
overlay = "."
|
||||||
|
crate_src_path = ".."
|
||||||
|
maintainer = "Proxmox Support Team <support@proxmox.com>"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
# TODO: update once public
|
||||||
|
vcs_git = ""
|
||||||
|
vcs_browser = ""
|
116
proxmox-openid/src/auth_state.rs
Normal file
116
proxmox-openid/src/auth_state.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{bail, Error};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use proxmox_sys::fs::{
|
||||||
|
replace_file,
|
||||||
|
open_file_locked,
|
||||||
|
file_get_json,
|
||||||
|
CreateOptions,
|
||||||
|
};
|
||||||
|
use proxmox_time::epoch_i64;
|
||||||
|
|
||||||
|
use super::{PublicAuthState, PrivateAuthState};
|
||||||
|
|
||||||
|
fn load_auth_state_locked(
|
||||||
|
state_dir: &Path,
|
||||||
|
realm: &str,
|
||||||
|
default: Option<Value>,
|
||||||
|
) -> Result<(PathBuf, std::fs::File, Vec<Value>), Error> {
|
||||||
|
|
||||||
|
let mut lock_path = state_dir.to_owned();
|
||||||
|
lock_path.push(format!("proxmox-openid-auth-state-{}.lck", realm));
|
||||||
|
|
||||||
|
let lock = open_file_locked(
|
||||||
|
lock_path,
|
||||||
|
std::time::Duration::new(10, 0),
|
||||||
|
true,
|
||||||
|
CreateOptions::new()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut path = state_dir.to_owned();
|
||||||
|
path.push(format!("proxmox-openid-auth-state-{}", realm));
|
||||||
|
|
||||||
|
let now = epoch_i64();
|
||||||
|
|
||||||
|
let old_data = file_get_json(&path, default)?;
|
||||||
|
|
||||||
|
let mut data: Vec<Value> = Vec::new();
|
||||||
|
|
||||||
|
let timeout = 10*60; // 10 minutes
|
||||||
|
|
||||||
|
for v in old_data.as_array().unwrap() {
|
||||||
|
let ctime = v["ctime"].as_i64().unwrap_or(0);
|
||||||
|
if (ctime + timeout) < now {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
data.push(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((path, lock, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_auth_state(
|
||||||
|
path: &Path,
|
||||||
|
data: &Vec<Value>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
|
||||||
|
let options = CreateOptions::new().perm(mode);
|
||||||
|
let raw = serde_json::to_string_pretty(data)?;
|
||||||
|
|
||||||
|
replace_file(path, raw.as_bytes(), options, false)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_public_auth_state(
|
||||||
|
state_dir: &Path,
|
||||||
|
state: &str,
|
||||||
|
) -> Result<(String, PrivateAuthState), Error> {
|
||||||
|
|
||||||
|
let public_auth_state: PublicAuthState = serde_json::from_str(state)?;
|
||||||
|
|
||||||
|
let (path, _lock, old_data) = load_auth_state_locked(state_dir, &public_auth_state.realm, None)?;
|
||||||
|
|
||||||
|
let mut data: Vec<Value> = Vec::new();
|
||||||
|
|
||||||
|
let mut entry: Option<PrivateAuthState> = None;
|
||||||
|
let find_csrf_token = public_auth_state.csrf_token.secret();
|
||||||
|
for v in old_data {
|
||||||
|
if v["csrf_token"].as_str() == Some(find_csrf_token) {
|
||||||
|
entry = Some(serde_json::from_value(v)?);
|
||||||
|
} else {
|
||||||
|
data.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = match entry {
|
||||||
|
None => bail!("no openid auth state found (possible timeout)"),
|
||||||
|
Some(entry) => entry,
|
||||||
|
};
|
||||||
|
|
||||||
|
replace_auth_state(&path, &data)?;
|
||||||
|
|
||||||
|
Ok((public_auth_state.realm, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_auth_state(
|
||||||
|
state_dir: &Path,
|
||||||
|
realm: &str,
|
||||||
|
auth_state: &PrivateAuthState,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let (path, _lock, mut data) = load_auth_state_locked(state_dir, realm, Some(json!([])))?;
|
||||||
|
|
||||||
|
if data.len() > 100 {
|
||||||
|
bail!("too many pending openid auth request for realm {}", realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push(serde_json::to_value(&auth_state)?);
|
||||||
|
|
||||||
|
replace_auth_state(&path, &data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
100
proxmox-openid/src/http_client.rs
Normal file
100
proxmox-openid/src/http_client.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use http::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
|
||||||
|
use http::method::Method;
|
||||||
|
use http::status::StatusCode;
|
||||||
|
|
||||||
|
use openidconnect::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
// Copied from OAuth2 create, because we want to use ureq with
|
||||||
|
// native-tls. But current OAuth2 crate pulls in rustls, so we cannot
|
||||||
|
// use their 'ureq' feature.
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Error type returned by failed ureq HTTP requests.
|
||||||
|
///
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Non-ureq HTTP error.
|
||||||
|
#[error("HTTP error - {0}")]
|
||||||
|
Http(#[from] http::Error),
|
||||||
|
|
||||||
|
/// IO error
|
||||||
|
#[error("IO error - {0}")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// Error returned by ureq crate.
|
||||||
|
// boxed due to https://github.com/algesten/ureq/issues/296
|
||||||
|
#[error("ureq request failed - {0}")]
|
||||||
|
Ureq(#[from] Box<ureq::Error>),
|
||||||
|
|
||||||
|
#[error("TLS error - {0}")]
|
||||||
|
Tls(#[from] native_tls::Error),
|
||||||
|
|
||||||
|
/// Other error.
|
||||||
|
#[error("Other error: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ureq_agent() -> Result<ureq::Agent, Error> {
|
||||||
|
let mut agent =
|
||||||
|
ureq::AgentBuilder::new().tls_connector(Arc::new(native_tls::TlsConnector::new()?));
|
||||||
|
if let Ok(val) = env::var("all_proxy").or_else(|_| env::var("ALL_PROXY")) {
|
||||||
|
let proxy = ureq::Proxy::new(val).map_err(Box::new)?;
|
||||||
|
agent = agent.proxy(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(agent.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Synchronous HTTP client for ureq.
|
||||||
|
///
|
||||||
|
pub fn http_client(request: HttpRequest) -> Result<HttpResponse, Error> {
|
||||||
|
let agent = ureq_agent()?;
|
||||||
|
let mut req = if let Method::POST = request.method {
|
||||||
|
agent.post(&request.url.to_string())
|
||||||
|
} else {
|
||||||
|
agent.get(&request.url.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
for (name, value) in request.headers {
|
||||||
|
if let Some(name) = name {
|
||||||
|
req = req.set(
|
||||||
|
&name.to_string(),
|
||||||
|
value.to_str().map_err(|_| {
|
||||||
|
Error::Other(format!(
|
||||||
|
"invalid {} header value {:?}",
|
||||||
|
name,
|
||||||
|
value.as_bytes()
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = if let Method::POST = request.method {
|
||||||
|
// send_bytes makes sure that Content-Length is set. This is important, because some
|
||||||
|
// endpoints don't accept `Transfer-Encoding: chunked`, which would otherwise be set.
|
||||||
|
// see https://docs.rs/ureq/2.4.0/ureq/index.html#content-length-and-transfer-encoding
|
||||||
|
req.send_bytes(request.body.as_slice())
|
||||||
|
} else {
|
||||||
|
req.call()
|
||||||
|
}
|
||||||
|
.map_err(Box::new)?;
|
||||||
|
|
||||||
|
let status_code =
|
||||||
|
StatusCode::from_u16(response.status()).map_err(|err| Error::Http(err.into()))?;
|
||||||
|
|
||||||
|
let content_type =
|
||||||
|
HeaderValue::from_str(response.content_type()).map_err(|err| Error::Http(err.into()))?;
|
||||||
|
|
||||||
|
Ok(HttpResponse {
|
||||||
|
status_code,
|
||||||
|
headers: vec![(CONTENT_TYPE, content_type)]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HeaderMap>(),
|
||||||
|
body: response.into_string()?.as_bytes().into(),
|
||||||
|
})
|
||||||
|
}
|
253
proxmox-openid/src/lib.rs
Normal file
253
proxmox-openid/src/lib.rs
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{format_err, Error};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
mod http_client;
|
||||||
|
pub use http_client::http_client;
|
||||||
|
|
||||||
|
mod auth_state;
|
||||||
|
pub use auth_state::*;
|
||||||
|
|
||||||
|
|
||||||
|
use openidconnect::{
|
||||||
|
//curl::http_client,
|
||||||
|
core::{
|
||||||
|
CoreProviderMetadata,
|
||||||
|
CoreClient,
|
||||||
|
CoreIdTokenClaims,
|
||||||
|
CoreIdTokenVerifier,
|
||||||
|
CoreAuthenticationFlow,
|
||||||
|
CoreAuthDisplay,
|
||||||
|
CoreAuthPrompt,
|
||||||
|
CoreGenderClaim,
|
||||||
|
},
|
||||||
|
PkceCodeChallenge,
|
||||||
|
PkceCodeVerifier,
|
||||||
|
AuthorizationCode,
|
||||||
|
ClientId,
|
||||||
|
ClientSecret,
|
||||||
|
CsrfToken,
|
||||||
|
IssuerUrl,
|
||||||
|
Nonce,
|
||||||
|
OAuth2TokenResponse,
|
||||||
|
RedirectUrl,
|
||||||
|
Scope,
|
||||||
|
UserInfoClaims,
|
||||||
|
AdditionalClaims,
|
||||||
|
AuthenticationContextClass,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Stores Additional Claims into a serde_json::Value;
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct GenericClaims(Value);
|
||||||
|
impl AdditionalClaims for GenericClaims {}
|
||||||
|
|
||||||
|
pub type GenericUserInfoClaims = UserInfoClaims<GenericClaims, CoreGenderClaim>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct OpenIdConfig {
|
||||||
|
pub issuer_url: String,
|
||||||
|
pub client_id: String,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub client_key: Option<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub scopes: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub acr_values: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpenIdAuthenticator {
|
||||||
|
client: CoreClient,
|
||||||
|
config: OpenIdConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct PublicAuthState {
|
||||||
|
pub csrf_token: CsrfToken,
|
||||||
|
pub realm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct PrivateAuthState {
|
||||||
|
pub csrf_token: CsrfToken,
|
||||||
|
pub nonce: Nonce,
|
||||||
|
pub pkce_verifier: PkceCodeVerifier,
|
||||||
|
pub ctime: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivateAuthState {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let nonce = Nonce::new_random();
|
||||||
|
let csrf_token = CsrfToken::new_random();
|
||||||
|
let (_pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
|
||||||
|
PrivateAuthState {
|
||||||
|
csrf_token,
|
||||||
|
nonce,
|
||||||
|
pkce_verifier,
|
||||||
|
ctime: proxmox_time::epoch_i64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pkce_verifier(&self) -> PkceCodeVerifier {
|
||||||
|
// Note: PkceCodeVerifier does not impl. clone()
|
||||||
|
PkceCodeVerifier::new(self.pkce_verifier.secret().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pkce_challenge(&self) -> PkceCodeChallenge {
|
||||||
|
PkceCodeChallenge::from_code_verifier_sha256(&self.pkce_verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_state_string(&self, realm: String) -> Result<String, Error> {
|
||||||
|
let pub_state = PublicAuthState {
|
||||||
|
csrf_token: self.csrf_token.clone(),
|
||||||
|
realm,
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&pub_state)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenIdAuthenticator {
|
||||||
|
|
||||||
|
pub fn discover(config: &OpenIdConfig, redirect_url: &str) -> Result<Self, Error> {
|
||||||
|
|
||||||
|
let client_id = ClientId::new(config.client_id.clone());
|
||||||
|
let client_key = config.client_key.clone().map(|key| ClientSecret::new(key));
|
||||||
|
let issuer_url = IssuerUrl::new(config.issuer_url.clone())?;
|
||||||
|
|
||||||
|
let provider_metadata = CoreProviderMetadata::discover(&issuer_url, http_client)?;
|
||||||
|
|
||||||
|
let client = CoreClient::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
client_id,
|
||||||
|
client_key,
|
||||||
|
).set_redirect_uri(RedirectUrl::new(String::from(redirect_url))?);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
config: config.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorize_url(&self, state_dir: &str, realm: &str) -> Result<String, Error> {
|
||||||
|
|
||||||
|
let private_auth_state = PrivateAuthState::new();
|
||||||
|
let public_auth_state = private_auth_state.public_state_string(realm.to_string())?;
|
||||||
|
let nonce = private_auth_state.nonce.clone();
|
||||||
|
|
||||||
|
store_auth_state(Path::new(state_dir), realm, &private_auth_state)?;
|
||||||
|
|
||||||
|
// Generate the authorization URL to which we'll redirect the user.
|
||||||
|
let mut request = self.client
|
||||||
|
.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
|| CsrfToken::new(public_auth_state),
|
||||||
|
|| nonce,
|
||||||
|
)
|
||||||
|
.set_pkce_challenge(private_auth_state.pkce_challenge());
|
||||||
|
|
||||||
|
request = request.set_display(CoreAuthDisplay::Page);
|
||||||
|
|
||||||
|
match self.config.prompt.as_deref() {
|
||||||
|
None => { /* nothing */ },
|
||||||
|
Some("none") => {
|
||||||
|
request = request.add_prompt(CoreAuthPrompt::None);
|
||||||
|
}
|
||||||
|
Some("login") => {
|
||||||
|
request = request.add_prompt(CoreAuthPrompt::Login);
|
||||||
|
}
|
||||||
|
Some("consent") => {
|
||||||
|
request = request.add_prompt(CoreAuthPrompt::Consent);
|
||||||
|
}
|
||||||
|
Some("select_account") => {
|
||||||
|
request = request.add_prompt(CoreAuthPrompt::SelectAccount);
|
||||||
|
}
|
||||||
|
Some(extension) => {
|
||||||
|
request = request.add_prompt(CoreAuthPrompt::Extension(extension.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref scopes) = self.config.scopes {
|
||||||
|
for scope in scopes.clone() {
|
||||||
|
request = request.add_scope(Scope::new(scope));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref acr_values) = self.config.acr_values {
|
||||||
|
for acr in acr_values.clone() {
|
||||||
|
request = request.add_auth_context_value(AuthenticationContextClass::new(acr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (authorize_url, _csrf_state, _nonce) = request.url();
|
||||||
|
|
||||||
|
Ok(authorize_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_public_auth_state(
|
||||||
|
state_dir: &str,
|
||||||
|
state: &str,
|
||||||
|
) -> Result<(String, PrivateAuthState), Error> {
|
||||||
|
verify_public_auth_state(Path::new(state_dir), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_authorization_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
private_auth_state: &PrivateAuthState,
|
||||||
|
) -> Result<(CoreIdTokenClaims, GenericUserInfoClaims), Error> {
|
||||||
|
|
||||||
|
let code = AuthorizationCode::new(code.to_string());
|
||||||
|
// Exchange the code with a token.
|
||||||
|
let token_response = self.client
|
||||||
|
.exchange_code(code)
|
||||||
|
.set_pkce_verifier(private_auth_state.pkce_verifier())
|
||||||
|
.request(http_client)
|
||||||
|
.map_err(|err| format_err!("Failed to contact token endpoint: {}", err))?;
|
||||||
|
|
||||||
|
let id_token_verifier: CoreIdTokenVerifier = self.client.id_token_verifier();
|
||||||
|
let id_token_claims: &CoreIdTokenClaims = token_response
|
||||||
|
.extra_fields()
|
||||||
|
.id_token()
|
||||||
|
.expect("Server did not return an ID token")
|
||||||
|
.claims(&id_token_verifier, &private_auth_state.nonce)
|
||||||
|
.map_err(|err| format_err!("Failed to verify ID token: {}", err))?;
|
||||||
|
|
||||||
|
let userinfo_claims: GenericUserInfoClaims = self.client
|
||||||
|
.user_info(token_response.access_token().to_owned(), None)?
|
||||||
|
.request(http_client)
|
||||||
|
.map_err(|err| format_err!("Failed to contact userinfo endpoint: {}", err))?;
|
||||||
|
|
||||||
|
Ok((id_token_claims.clone(), userinfo_claims))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like verify_authorization_code(), but returns claims as serde_json::Value
|
||||||
|
pub fn verify_authorization_code_simple(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
private_auth_state: &PrivateAuthState,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
|
||||||
|
let (id_token_claims, userinfo_claims) = self.verify_authorization_code(&code, &private_auth_state)?;
|
||||||
|
|
||||||
|
let mut data = serde_json::to_value(id_token_claims)?;
|
||||||
|
|
||||||
|
let data2 = serde_json::to_value(userinfo_claims)?;
|
||||||
|
|
||||||
|
if let Some(map) = data2.as_object() {
|
||||||
|
for (key, value) in map {
|
||||||
|
if data[key] != Value::Null {
|
||||||
|
continue; // already set
|
||||||
|
}
|
||||||
|
data[key] = value.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user