Added webfinger implementation

This commit is contained in:
CEbbinghaus 2025-02-09 18:37:24 +11:00
parent b96fe49b99
commit d88005a1c6
4 changed files with 116 additions and 2 deletions
proto/src
server
core/src
lib/src/idm

View file

@ -443,6 +443,21 @@ fn require_request_uri_parameter_supported_default() -> bool {
false
}
#[derive(Serialize, Deserialize, Debug)]
pub struct OidcWebfingerRel {
pub rel: String,
pub href: String,
}
/// The response to an Webfinger request. Only a subset of the body is defined here.
/// <https://datatracker.ietf.org/doc/html/rfc7033>
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct OidcWebfingerResponse {
pub subject: String,
pub links: Vec<OidcWebfingerRel>,
}
/// The response to an OpenID connect discovery request
/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
#[skip_serializing_none]

View file

@ -9,6 +9,7 @@ use kanidm_proto::internal::{
IdentifyUserRequest, IdentifyUserResponse, ImageValue, OperationError, RadiusAuthToken,
SearchRequest, SearchResponse, UserAuthToken,
};
use kanidm_proto::oauth2::OidcWebfingerResponse;
use kanidm_proto::v1::{
AuthIssueSession, AuthRequest, Entry as ProtoEntry, UatStatus, UnixGroupToken, UnixUserToken,
WhoamiResponse,
@ -1509,6 +1510,21 @@ impl QueryServerReadV1 {
idms_prox_read.oauth2_openid_discovery(&client_id)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_webfinger_discovery(
&self,
client_id: String,
resource_id: String,
eventid: Uuid,
) -> Result<OidcWebfingerResponse, OperationError> {
let idms_prox_read = self.idms.proxy_read().await?;
idms_prox_read.oauth2_openid_webfinger_discovery(&client_id, &resource_id)
}
#[instrument(
level = "info",
skip_all,

View file

@ -513,7 +513,7 @@ pub async fn oauth2_token_post(
}
}
// // For future openid integration
// For future openid integration
pub async fn oauth2_openid_discovery_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
@ -538,6 +538,45 @@ pub async fn oauth2_openid_discovery_get(
}
}
pub async fn oauth2_openid_webfinger_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
Query(resource): Query<String>,
// For the moment this is implemented ignoring the rel's
// Query(rel): Query<Vec<String>>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let cleaned_resource = match resource {
s if s.starts_with("acct:") => s[5..].to_string(),
s => s,
};
let res = state
.qe_r_ref
.handle_oauth2_webfinger_discovery(client_id, cleaned_resource, kopid.eventid)
.await;
match res {
Ok(dsc) => (
StatusCode::OK,
[
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
(CONTENT_TYPE, "application/jrd+json"),
],
Json({
dsc.subject = resource;
dsc
}),
)
.into_response(),
Err(e) => {
error!(err = ?e, "Unable to access discovery info");
WebError::from(e).response_with_access_control_origin_header()
}
}
}
pub async fn oauth2_rfc8414_metadata_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
@ -770,6 +809,10 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
"/oauth2/openid/:client_id/.well-known/openid-configuration",
get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
)
.route(
"/oauth2/openid/:client_id/.well-known/webfinger",
get(oauth2_openid_webfinger_get).options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(

View file

@ -32,7 +32,7 @@ pub use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse,
OidcDiscoveryResponse, PkceAlg, TokenRevokeRequest,
OidcDiscoveryResponse, OidcWebfingerRel, OidcWebfingerResponse, PkceAlg, TokenRevokeRequest,
};
use kanidm_proto::oauth2::{
@ -2742,6 +2742,46 @@ impl IdmServerProxyReadTransaction<'_> {
})
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_openid_webfinger_discovery(
&self,
client_id: &str,
resource_id: &str,
) -> Result<OidcWebfingerResponse, OperationError> {
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
);
OperationError::NoMatchingEntries
})?;
let Some(spn) = PartialValue::new_spn_s(resource_id) else {
return Err(OperationError::NoMatchingEntries);
};
// Ensure that the account exists. If we consider account's spn privileged information
// We can shortcut this step and always return valid. This means it will seem like every user exits
// However it will fail at logon. I don't think leaking spn's is a problem however
if !self
.qs_read
.internal_exists(Filter::new(f_eq(Attribute::Spn, spn)))
{
return Err(OperationError::NoMatchingEntries);
}
let issuer = o2rs.iss.clone();
Ok(OidcWebfingerResponse {
// we set the subject to the resource_id to ensure we always send something valid back
// but realistically this will be overwritten on at the API layer
subject: resource_id.to_string(),
links: vec![OidcWebfingerRel {
rel: "http://openid.net/specs/connect/1.0/issuer".into(),
href: issuer.into(),
}],
})
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_openid_publickey(&self, client_id: &str) -> Result<JwkKeySet, OperationError> {
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {