user: Allow adding principals by group membership
dustin/sshca/pipeline/head This commit looks good
Details
dustin/sshca/pipeline/head This commit looks good
Details
In some cases, users may need to authenticate as a different user on the remote machine than their normal username. For example, the default user *core* on a Fedora CoreOS machine, or the *root* user on machines that have not been provisioned yet. In such cases, the default set of principals on issued user certificates is not sufficient. We don't want to allow users to specify arbitrary principals, so instead we can use their membership in specific groups to add a preselected set of principals. Since the `groups` claim is not part of the core OpenID Connect specification, we have to define it ourselves as part of the "additional claims" of the token. This is somewhat cumbersome and involves a lot of copying from the core type aliases, but otherwise straightforward.master
parent
f9ebbbcce9
commit
748f4dba9a
|
@ -1,4 +1,5 @@
|
||||||
//! Application configuration
|
//! Application configuration
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
@ -79,6 +80,10 @@ pub struct UserCaConfig {
|
||||||
/// Certificate extensions
|
/// Certificate extensions
|
||||||
#[serde(default = "default_user_cert_extensions")]
|
#[serde(default = "default_user_cert_extensions")]
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
|
|
||||||
|
/// Additional principals to add based on user's group membership
|
||||||
|
#[serde(default)]
|
||||||
|
pub group_principals: HashMap<String, Vec<String>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserCaConfig {
|
impl Default for UserCaConfig {
|
||||||
|
@ -88,6 +93,7 @@ impl Default for UserCaConfig {
|
||||||
private_key_passphrase_file: None,
|
private_key_passphrase_file: None,
|
||||||
cert_duration: default_user_cert_duration(),
|
cert_duration: default_user_cert_duration(),
|
||||||
extensions: default_user_cert_extensions(),
|
extensions: default_user_cert_extensions(),
|
||||||
|
group_principals: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod error;
|
mod error;
|
||||||
mod host;
|
mod host;
|
||||||
|
mod oidc;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
use openidconnect::core::*;
|
||||||
|
use openidconnect::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub type IdTokenFields = openidconnect::IdTokenFields<
|
||||||
|
AdditionalClaims,
|
||||||
|
EmptyExtraTokenFields,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type IdToken = openidconnect::IdToken<
|
||||||
|
AdditionalClaims,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type IdTokenClaims =
|
||||||
|
openidconnect::IdTokenClaims<AdditionalClaims, CoreGenderClaim>;
|
||||||
|
|
||||||
|
pub type TokenResponse = StandardTokenResponse<IdTokenFields, CoreTokenType>;
|
||||||
|
|
||||||
|
pub type Client = openidconnect::Client<
|
||||||
|
AdditionalClaims,
|
||||||
|
CoreAuthDisplay,
|
||||||
|
CoreGenderClaim,
|
||||||
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
CoreJsonWebKeyUse,
|
||||||
|
CoreJsonWebKey,
|
||||||
|
CoreAuthPrompt,
|
||||||
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
TokenResponse,
|
||||||
|
CoreTokenType,
|
||||||
|
CoreTokenIntrospectionResponse,
|
||||||
|
CoreRevocableToken,
|
||||||
|
CoreRevocationErrorResponse,
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct AdditionalClaims {
|
||||||
|
groups: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdditionalClaims {
|
||||||
|
pub fn groups(&self) -> &Vec<String> {
|
||||||
|
&self.groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl openidconnect::AdditionalClaims for AdditionalClaims {}
|
|
@ -12,8 +12,7 @@ use axum::headers::Authorization;
|
||||||
use axum::http::request::Parts;
|
use axum::http::request::Parts;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::{RequestPartsExt, TypedHeader};
|
use axum::{RequestPartsExt, TypedHeader};
|
||||||
use openidconnect::core::{CoreClient, CoreProviderMetadata};
|
use openidconnect::core::CoreProviderMetadata;
|
||||||
use openidconnect::core::{CoreIdToken, CoreIdTokenClaims};
|
|
||||||
use openidconnect::reqwest::async_http_client;
|
use openidconnect::reqwest::async_http_client;
|
||||||
use openidconnect::IssuerUrl;
|
use openidconnect::IssuerUrl;
|
||||||
use openidconnect::Nonce;
|
use openidconnect::Nonce;
|
||||||
|
@ -24,6 +23,7 @@ use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use super::error::SignKeyError;
|
use super::error::SignKeyError;
|
||||||
use super::{AuthError, Context};
|
use super::{AuthError, Context};
|
||||||
|
use super::oidc;
|
||||||
use crate::ca;
|
use crate::ca;
|
||||||
|
|
||||||
/// Response type for GET /user/openid-config
|
/// Response type for GET /user/openid-config
|
||||||
|
@ -38,7 +38,7 @@ pub struct OidcConfigResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OpenID Connect ID token claims
|
/// OpenID Connect ID token claims
|
||||||
pub struct Claims(CoreIdTokenClaims);
|
pub struct Claims(oidc::IdTokenClaims);
|
||||||
|
|
||||||
/// Axum request extractor for OIDC ID tokens in Authorization headers
|
/// Axum request extractor for OIDC ID tokens in Authorization headers
|
||||||
///
|
///
|
||||||
|
@ -69,7 +69,7 @@ impl FromRequestParts<Arc<Context>> for Claims {
|
||||||
AuthError
|
AuthError
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let token = CoreIdToken::from_str(bearer.token()).map_err(|e| {
|
let token = oidc::IdToken::from_str(bearer.token()).map_err(|e| {
|
||||||
debug!("Failed to parse OIDC ID token: {}", e);
|
debug!("Failed to parse OIDC ID token: {}", e);
|
||||||
AuthError
|
AuthError
|
||||||
})?;
|
})?;
|
||||||
|
@ -77,7 +77,7 @@ impl FromRequestParts<Arc<Context>> for Claims {
|
||||||
let client_id = &oidc_config.client_id;
|
let client_id = &oidc_config.client_id;
|
||||||
let client_secret = &oidc_config.client_secret;
|
let client_secret = &oidc_config.client_secret;
|
||||||
let provider_metadata = get_metadata(ctx).await.ok_or(AuthError)?;
|
let provider_metadata = get_metadata(ctx).await.ok_or(AuthError)?;
|
||||||
let client = CoreClient::from_provider_metadata(
|
let client = oidc::Client::from_provider_metadata(
|
||||||
provider_metadata,
|
provider_metadata,
|
||||||
ClientId::new(client_id.into()),
|
ClientId::new(client_id.into()),
|
||||||
client_secret.as_ref().map(|s| ClientSecret::new(s.into())),
|
client_secret.as_ref().map(|s| ClientSecret::new(s.into())),
|
||||||
|
@ -169,6 +169,16 @@ pub(super) async fn sign_user_cert(
|
||||||
let config = &ctx.config;
|
let config = &ctx.config;
|
||||||
let duration = Duration::from_secs(config.ca.user.cert_duration);
|
let duration = Duration::from_secs(config.ca.user.cert_duration);
|
||||||
let extensions = &config.ca.user.extensions;
|
let extensions = &config.ca.user.extensions;
|
||||||
|
|
||||||
|
for group in claims.additional_claims().groups() {
|
||||||
|
if let Some(principals) = config.ca.user.group_principals.get(group) {
|
||||||
|
debug!("Adding principals from group {}", group);
|
||||||
|
for p in principals {
|
||||||
|
alias.push(p.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let privkey = ca::load_private_key(
|
let privkey = ca::load_private_key(
|
||||||
&config.ca.user.private_key_file,
|
&config.ca.user.private_key_file,
|
||||||
config.ca.user.private_key_passphrase_file.as_ref(),
|
config.ca.user.private_key_passphrase_file.as_ref(),
|
||||||
|
|
Loading…
Reference in New Issue