initial import
This commit is contained in:
commit
ca65d297b7
5
.cargo/config
Normal file
5
.cargo/config
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[source]
|
||||||
|
[source.debian-packages]
|
||||||
|
directory = "/usr/share/cargo/registry"
|
||||||
|
[source.crates-io]
|
||||||
|
replace-with = "debian-packages"
|
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "proxmox-openid"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "proxmox_openid"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
url = "2.1"
|
||||||
|
http = "0.2"
|
||||||
|
curl = { version = "0.4.33" }
|
||||||
|
proxmox = { version = "0.11.5", features = [ "sortable-macro", "api-macro" ] }
|
||||||
|
nix = "0.19.1"
|
||||||
|
openidconnect = { version = "2.0", default-features = false, features = ["curl"] }
|
107
src/auth_state.rs
Normal file
107
src/auth_state.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use anyhow::{bail, Error};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use nix::unistd::Uid;
|
||||||
|
|
||||||
|
use proxmox::tools::{
|
||||||
|
time::epoch_i64,
|
||||||
|
fs::{
|
||||||
|
replace_file,
|
||||||
|
open_file_locked,
|
||||||
|
file_get_json,
|
||||||
|
CreateOptions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{PublicAuthState, PrivateAuthState};
|
||||||
|
|
||||||
|
fn load_auth_state_locked(realm: &str, default: Option<Value>) -> Result<(String, std::fs::File, Vec<Value>), Error> {
|
||||||
|
|
||||||
|
let lock = open_file_locked(
|
||||||
|
"/tmp/proxmox-openid-auth-state.lock",
|
||||||
|
std::time::Duration::new(10, 0),
|
||||||
|
true
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let path = format!("/tmp/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: &str, data: &Vec<Value>, state_owner: Uid) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
|
||||||
|
// set the correct owner/group/permissions while saving file
|
||||||
|
// owner(rw) = root
|
||||||
|
let options = CreateOptions::new()
|
||||||
|
.perm(mode)
|
||||||
|
.owner(state_owner);
|
||||||
|
|
||||||
|
let raw = serde_json::to_string_pretty(data)?;
|
||||||
|
|
||||||
|
replace_file(&path, raw.as_bytes(), options)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_public_auth_state(state: &str, state_owner: Uid) -> Result<(String, PrivateAuthState), Error> {
|
||||||
|
|
||||||
|
let public_auth_state: PublicAuthState = serde_json::from_str(state)?;
|
||||||
|
|
||||||
|
let (path, _lock, old_data) = load_auth_state_locked(&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, state_owner)?;
|
||||||
|
|
||||||
|
Ok((public_auth_state.realm, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_auth_state(
|
||||||
|
realm: &str,
|
||||||
|
auth_state: &PrivateAuthState,
|
||||||
|
state_owner: Uid,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let (path, _lock, mut data) = load_auth_state_locked(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, state_owner)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
92
src/http_client.rs
Normal file
92
src/http_client.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use curl::easy::Easy;
|
||||||
|
use http::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
|
||||||
|
use http::method::Method;
|
||||||
|
use http::status::StatusCode;
|
||||||
|
|
||||||
|
use openidconnect::{
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Synchronous Curl HTTP client.
|
||||||
|
///
|
||||||
|
/// Copied fron OAuth2 create, added fix https://github.com/ramosbugs/oauth2-rs/pull/147
|
||||||
|
pub fn http_client(request: HttpRequest) -> Result<HttpResponse, openidconnect::curl::Error> {
|
||||||
|
|
||||||
|
use openidconnect::curl::Error;
|
||||||
|
|
||||||
|
let mut easy = Easy::new();
|
||||||
|
easy.url(&request.url.to_string()[..])
|
||||||
|
.map_err(Error::Curl)?;
|
||||||
|
|
||||||
|
let mut headers = curl::easy::List::new();
|
||||||
|
request
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| {
|
||||||
|
headers
|
||||||
|
.append(&format!(
|
||||||
|
"{}: {}",
|
||||||
|
name,
|
||||||
|
value.to_str().map_err(|_| Error::Other(format!(
|
||||||
|
"invalid {} header value {:?}",
|
||||||
|
name,
|
||||||
|
value.as_bytes()
|
||||||
|
)))?
|
||||||
|
))
|
||||||
|
.map_err(Error::Curl)
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
easy.http_headers(headers).map_err(Error::Curl)?;
|
||||||
|
|
||||||
|
if let Method::POST = request.method {
|
||||||
|
easy.post(true).map_err(Error::Curl)?;
|
||||||
|
easy.post_field_size(request.body.len() as u64)
|
||||||
|
.map_err(Error::Curl)?;
|
||||||
|
} else {
|
||||||
|
assert_eq!(request.method, Method::GET);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut form_slice = &request.body[..];
|
||||||
|
let mut data = Vec::new();
|
||||||
|
{
|
||||||
|
let mut transfer = easy.transfer();
|
||||||
|
|
||||||
|
transfer
|
||||||
|
.read_function(|buf| Ok(form_slice.read(buf).unwrap_or(0)))
|
||||||
|
.map_err(Error::Curl)?;
|
||||||
|
|
||||||
|
transfer
|
||||||
|
.write_function(|new_data| {
|
||||||
|
data.extend_from_slice(new_data);
|
||||||
|
Ok(new_data.len())
|
||||||
|
})
|
||||||
|
.map_err(Error::Curl)?;
|
||||||
|
|
||||||
|
transfer.perform().map_err(Error::Curl)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_code = easy.response_code().map_err(Error::Curl)? as u16;
|
||||||
|
|
||||||
|
Ok(HttpResponse {
|
||||||
|
status_code: StatusCode::from_u16(status_code).map_err(|err| Error::Http(err.into()))?,
|
||||||
|
headers: easy
|
||||||
|
.content_type()
|
||||||
|
.map_err(Error::Curl)?
|
||||||
|
.map(|content_type| {
|
||||||
|
Ok(vec![(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(content_type).map_err(|err| Error::Http(err.into()))?,
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HeaderMap>())
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_else(HeaderMap::new),
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
181
src/lib.rs
Normal file
181
src/lib.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use anyhow::{format_err, Error};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use nix::unistd::Uid;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
PkceCodeChallenge,
|
||||||
|
PkceCodeVerifier,
|
||||||
|
AuthorizationCode,
|
||||||
|
ClientId,
|
||||||
|
ClientSecret,
|
||||||
|
CsrfToken,
|
||||||
|
IssuerUrl,
|
||||||
|
Nonce,
|
||||||
|
OAuth2TokenResponse,
|
||||||
|
RedirectUrl,
|
||||||
|
Scope,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct OpenIdConfig {
|
||||||
|
pub issuer_url: String,
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpenIdAuthenticator {
|
||||||
|
client: CoreClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::tools::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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorize_url(&self, realm: &str, state_owner: Uid) -> Result<Url, 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(realm, &private_auth_state, state_owner)?;
|
||||||
|
|
||||||
|
// Generate the authorization URL to which we'll redirect the user.
|
||||||
|
let (authorize_url, _csrf_state, _nonce) = self.client
|
||||||
|
.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
|| CsrfToken::new(public_auth_state),
|
||||||
|
|| nonce,
|
||||||
|
)
|
||||||
|
.set_display(CoreAuthDisplay::Page)
|
||||||
|
.add_prompt(CoreAuthPrompt::Login)
|
||||||
|
.add_scope(Scope::new("email".to_string()))
|
||||||
|
.add_scope(Scope::new("profile".to_string()))
|
||||||
|
.set_pkce_challenge(private_auth_state.pkce_challenge())
|
||||||
|
.url();
|
||||||
|
|
||||||
|
Ok(authorize_url.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_public_auth_state(
|
||||||
|
state: &str,
|
||||||
|
state_owner: Uid,
|
||||||
|
) -> Result<(String, PrivateAuthState), Error> {
|
||||||
|
verify_public_auth_state(state, state_owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_authorization_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
private_auth_state: &PrivateAuthState,
|
||||||
|
) -> Result<CoreIdTokenClaims, 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))?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"OpenId returned access token:\n{}\n",
|
||||||
|
token_response.access_token().secret()
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("OpenId returned scopes: {:?}", token_response.scopes());
|
||||||
|
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
println!("Google returned ID token: {:?}", id_token_claims);
|
||||||
|
|
||||||
|
Ok(id_token_claims.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user