From 188a21342f4908a269ef4e317eac3a4f8afc6c15 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Mon, 7 Sep 2026 00:09:02 +0200 Subject: [PATCH] feat(service): Add OIDC service Signed-off-by: Louis Chmn --- README.md | 1 + blueprints/authentik-oidc.yaml | 71 ++++++++++++++++++++++++++++ src/service.rs | 9 +++- src/service/authentik/mod.rs | 33 +++++++++++++ src/service/authentik/oidc.rs | 86 ++++++++++++++++++++++++++++++++++ src/service/authentik/saml.rs | 16 +------ 6 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 blueprints/authentik-oidc.yaml create mode 100644 src/service/authentik/oidc.rs diff --git a/README.md b/README.md index acf3ce7..ccde18e 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Additionally, you can use the following options when starting an instance: - `s3m`: enable multi-instance S3 setup. - `ldap`: set up an LDAP server. - `saml`: set up authentik as a SAML IDP. +- `oidc`: set up authentik as an OIDC IDP. - `office`: set up a Nextcloud Office server. - `onlyoffice` setup an onlyoffice document server. - `push` set up [client push](https://github.com/nextcloud/notify_push). diff --git a/blueprints/authentik-oidc.yaml b/blueprints/authentik-oidc.yaml new file mode 100644 index 0000000..b50e9d8 --- /dev/null +++ b/blueprints/authentik-oidc.yaml @@ -0,0 +1,71 @@ +# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json +version: 1 +metadata: + name: haze-nextcloud-oidc +entries: + - model: authentik_crypto.certificatekeypair + id: haze-oidc-cert + identifiers: + name: haze-oidc + attrs: + certificate_data: !File /haze/authentik/public.crt + key_data: !File /haze/authentik/private.key + + # Prefix OIDC users with 'oidc-' so that they don't collide with local users. + - model: authentik_providers_oauth2.scopemapping + id: haze-nextcloud-oidc-uid + identifiers: + name: haze-nextcloud-oidc-uid + attrs: + scope_name: nextcloud + expression: 'return {"nextcloud_uid": "oidc-" + request.user.username}' + + - model: authentik_providers_oauth2.oauth2provider + id: nextcloud-oidc-provider + identifiers: + name: nextcloud-oidc + attrs: + client_type: confidential + client_id: nextcloud + client_secret: haze-oidc-secret + grant_types: + - authorization_code + - refresh_token + redirect_uris: + - matching_mode: strict + url: + !Format [ + "%s/index.php/apps/user_oidc/code", + !File /haze/authentik/nextcloud-url, + ] + signing_key: !KeyOf haze-oidc-cert + authorization_flow: + !Find [ + authentik_flows.flow, + [slug, default-provider-authorization-implicit-consent], + ] + invalidation_flow: + !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + property_mappings: + - !KeyOf haze-nextcloud-oidc-uid + - !Find [ + authentik_providers_oauth2.scopemapping, + [managed, goauthentik.io/providers/oauth2/scope-openid], + ] + - !Find [ + authentik_providers_oauth2.scopemapping, + [managed, goauthentik.io/providers/oauth2/scope-email], + ] + - !Find [ + authentik_providers_oauth2.scopemapping, + [managed, goauthentik.io/providers/oauth2/scope-profile], + ] + + - model: authentik_core.application + identifiers: + slug: nextcloud-oidc + attrs: + name: Nextcloud with OIDC + provider: !KeyOf nextcloud-oidc-provider + meta_launch_url: !File /haze/authentik/nextcloud-url + meta_description: Nextcloud instance provisioned by haze diff --git a/src/service.rs b/src/service.rs index 0ab6411..d0bc08e 100644 --- a/src/service.rs +++ b/src/service.rs @@ -19,7 +19,7 @@ mod webhook; use crate::cloud::CloudOptions; use crate::config::{HazeConfig, Preset, ProxyConfig}; -pub use crate::service::authentik::{Authentik, AuthentikSaml}; +pub use crate::service::authentik::{Authentik, AuthentikOidc, AuthentikSaml}; pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket}; use crate::service::dav::Dav; use crate::service::imaginary::Imaginary; @@ -320,6 +320,8 @@ pub enum ServiceType { Authentik, /// Configure Authentik as a SAML IDP for Nextcloud Saml, + /// Configure Authentik as an OIDC IDP for Nextcloud + Oidc, } #[enum_dispatch] @@ -355,6 +357,7 @@ pub enum Service { Webhook(Webhook), Authentik(Authentik), AuthentikSaml(AuthentikSaml), + AuthentikOidc(AuthentikOidc), Preset(PresetService), } @@ -406,6 +409,10 @@ impl Service { Service::Authentik(Authentik), Service::AuthentikSaml(AuthentikSaml), ]), + ServiceType::Oidc => Some(vec![ + Service::Authentik(Authentik), + Service::AuthentikOidc(AuthentikOidc), + ]), } } else { presets diff --git a/src/service/authentik/mod.rs b/src/service/authentik/mod.rs index 5c24480..3a72568 100644 --- a/src/service/authentik/mod.rs +++ b/src/service/authentik/mod.rs @@ -1,5 +1,7 @@ +mod oidc; mod saml; +pub use oidc::AuthentikOidc; pub use saml::AuthentikSaml; use crate::Result; @@ -19,6 +21,9 @@ use std::net::{IpAddr, Ipv4Addr}; const AUTHENTIK_IMAGE: &str = "ghcr.io/goauthentik/server:2026.8.0"; const POSTGRES_IMAGE: &str = "docker.io/library/postgres:16-alpine"; +pub(super) const SIGNING_CERT: &str = include_str!("../../../certificates/authentik/public.crt"); +pub(super) const SIGNING_KEY: &str = include_str!("../../../certificates/authentik/private.key"); + pub(super) const AUTHENTIK_PORT: u16 = 9000; const AUTHENTIK_TOKEN: &str = "haze"; @@ -90,6 +95,9 @@ async fn spawn_authentik( let name = container_name(cloud_id, role); let haze_directory = config.work_dir.join(cloud_id).join("authentik"); + write_file(&haze_directory, "public.crt", SIGNING_CERT)?; + write_file(&haze_directory, "private.key", SIGNING_KEY)?; + let blueprints_directory = haze_directory.join("blueprints"); write_file(&blueprints_directory, "nextcloud.yaml", BLUEPRINT)?; @@ -236,6 +244,31 @@ impl ServiceTrait for Authentik { ]) } + async fn post_setup( + &self, + docker: &Docker, + cloud_id: &str, + config: &HazeConfig, + ) -> Result>> { + let authentik_container = container_name(cloud_id, "server"); + + let authentik_url = config.proxy.addr_with_port( + &authentik_container, + container_ip(docker, &authentik_container, None).await?, + AUTHENTIK_PORT, + ); + let nextcloud_url = config.proxy.addr( + cloud_id, + container_ip(docker, cloud_id, Some("haze")).await?, + ); + + let haze_directory = config.work_dir.join(cloud_id).join("authentik"); + write_file(&haze_directory, "authentik-url", &authentik_url)?; + write_file(&haze_directory, "nextcloud-url", &nextcloud_url)?; + + Ok(vec![]) + } + fn container_name(&self, cloud_id: &str) -> Option { Some(container_name(cloud_id, "server")) } diff --git a/src/service/authentik/oidc.rs b/src/service/authentik/oidc.rs new file mode 100644 index 0000000..197bb2f --- /dev/null +++ b/src/service/authentik/oidc.rs @@ -0,0 +1,86 @@ +use super::{AUTHENTIK_PORT, container_ip, container_name, write_file}; +use crate::Result; +use crate::cloud::CloudOptions; +use crate::config::HazeConfig; +use crate::service::{ServiceTrait, split_cmnd}; +use bollard::Docker; + +const BLUEPRINT: &str = include_str!("../../../blueprints/authentik-oidc.yaml"); + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct AuthentikOidc; + +#[async_trait::async_trait] +impl ServiceTrait for AuthentikOidc { + fn name(&self) -> &str { + "oidc" + } + + async fn spawn( + &self, + _docker: &Docker, + cloud_id: &str, + _network: &str, + config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + let blueprints_directory = config + .work_dir + .join(cloud_id) + .join("authentik") + .join("blueprints"); + write_file(&blueprints_directory, "oidc.yaml", BLUEPRINT)?; + + Ok(Vec::new()) + } + + fn apps(&self) -> &'static [&'static str] { + &["user_oidc"] + } + + async fn post_setup( + &self, + docker: &Docker, + cloud_id: &str, + config: &HazeConfig, + ) -> Result>> { + let authentik_container = container_name(cloud_id, "server"); + let authentik_url = config.proxy.addr_with_port( + &authentik_container, + container_ip(docker, &authentik_container, None).await?, + AUTHENTIK_PORT, + ); + + let allow_insecure = if config.proxy.https { "0" } else { "1" }; + + Ok(vec![ + split_cmnd(&format!( + "occ config:app:set --silent user_oidc allow_insecure_http --value {allow_insecure}" + )), + split_cmnd( + "occ config:app:set --silent user_oidc allow_multiple_user_backends --value 1", + ), + vec![ + "occ".into(), + "user_oidc:provider".into(), + "Authentik OIDC".into(), + "--clientid=nextcloud".into(), + "--clientsecret=haze-oidc-secret".into(), + format!( + "--discoveryuri={authentik_url}/application/o/nextcloud-oidc/.well-known/openid-configuration" + ), + format!( + "--endsessionendpointuri={authentik_url}/application/o/nextcloud-oidc/end-session/" + ), + "--scope".into(), + "openid email profile nextcloud".into(), + "--mapping-uid=nextcloud_uid".into(), + "--mapping-display-name=name".into(), + "--mapping-email=email".into(), + "--mapping-groups=groups".into(), + "--group-provisioning=1".into(), + "--unique-uid=0".into(), + ], + ]) + } +} diff --git a/src/service/authentik/saml.rs b/src/service/authentik/saml.rs index 870b2cf..b64dc7b 100644 --- a/src/service/authentik/saml.rs +++ b/src/service/authentik/saml.rs @@ -2,11 +2,10 @@ use super::{AUTHENTIK_PORT, container_ip, container_name, write_file}; use crate::Result; use crate::cloud::CloudOptions; use crate::config::{HazeConfig, ProxyConfig}; +use crate::service::authentik::SIGNING_CERT; use crate::service::{ServiceTrait, split_cmnd}; use bollard::Docker; -const SIGNING_CERT: &str = include_str!("../../../certificates/authentik/public.crt"); -const SIGNING_KEY: &str = include_str!("../../../certificates/authentik/private.key"); const BLUEPRINT: &str = include_str!("../../../blueprints/authentik-saml.yaml"); #[derive(Debug, Clone, Eq, PartialEq)] @@ -26,10 +25,6 @@ impl ServiceTrait for AuthentikSaml { config: &HazeConfig, _options: &CloudOptions, ) -> Result> { - let haze_directory = config.work_dir.join(cloud_id).join("authentik"); - write_file(&haze_directory, "public.crt", SIGNING_CERT)?; - write_file(&haze_directory, "private.key", SIGNING_KEY)?; - let blueprints_directory = config .work_dir .join(cloud_id) @@ -51,20 +46,11 @@ impl ServiceTrait for AuthentikSaml { config: &HazeConfig, ) -> Result>> { let authentik_container = container_name(cloud_id, "server"); - let authentik_url = config.proxy.addr_with_port( &authentik_container, container_ip(docker, &authentik_container, None).await?, AUTHENTIK_PORT, ); - let nextcloud_url = config.proxy.addr( - cloud_id, - container_ip(docker, cloud_id, Some("haze")).await?, - ); - - let haze_directory = config.work_dir.join(cloud_id).join("authentik"); - write_file(&haze_directory, "authentik-url", &authentik_url)?; - write_file(&haze_directory, "nextcloud-url", &nextcloud_url)?; Ok(vec![ split_cmnd("occ config:app:set --silent user_saml type --value saml"),