diff --git a/docs/redirects.json b/docs/redirects.json index 762e1b6..c3402a9 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -77,6 +77,15 @@ "blocks-authelia-options-shb.authelia.enable": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.enable" ], + "blocks-authelia-options-shb.authelia.extraDefinitions": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.extraDefinitions" + ], + "blocks-authelia-options-shb.authelia.extraOidcClaimsPolicies": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.extraOidcClaimsPolicies" + ], + "blocks-authelia-options-shb.authelia.extraOidcScopes": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.extraOidcScopes" + ], "blocks-authelia-options-shb.authelia.ldapHostname": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.ldapHostname" ], @@ -3041,6 +3050,9 @@ "services-nextcloudserver-options-shb.nextcloud.apps.sso": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso" ], + "services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup" + ], "services-nextcloudserver-options-shb.nextcloud.apps.sso.authorization_policy": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.authorization_policy" ], diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index 6de9eef..cb8a418 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -140,6 +140,24 @@ in }; }; + extraOidcClaimsPolicies = lib.mkOption { + description = "Extra OIDC claims policies."; + type = lib.types.attrsOf lib.types.attrs; + default = {}; + }; + + extraOidcScopes = lib.mkOption { + description = "Extra OIDC scopes."; + type = lib.types.attrsOf lib.types.attrs; + default = {}; + }; + + extraDefinitions = lib.mkOption { + description = "Extra definitions."; + type = lib.types.attrsOf lib.types.attrs; + default = {}; + }; + oidcClients = lib.mkOption { description = "OIDC clients"; default = [ @@ -205,13 +223,13 @@ in scopes = lib.mkOption { type = lib.types.listOf lib.types.str; - description = "Scopes to ask for"; + description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims"; example = [ "openid" "profile" "email" "groups" ]; default = []; }; claims_policy = lib.mkOption { - type = lib.types.str; + type = lib.types.nullOr lib.types.str; description = '' Claim policy. @@ -387,11 +405,6 @@ in user = "uid=admin,ou=people,${cfg.dcdomain}"; }; }; - # This should go away at some point. - # https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter - identity_providers.oidc = { - claims_policies.default.id_token = [ "email" "preferred_username" "name" "groups" ]; - }; totp = { disable = "false"; issuer = fqdnWithPort; @@ -465,6 +478,17 @@ in }; log.level = if cfg.debug then "debug" else "info"; + } // { + identity_providers.oidc = { + claims_policies = { + # This default claim should go away at some point. + # https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter + default.id_token = [ "email" "preferred_username" "name" "groups" ]; + } // cfg.extraOidcClaimsPolicies; + scopes = cfg.extraOidcScopes; + }; + } // lib.optionalAttrs (cfg.extraDefinitions != {}) { + definitions = cfg.extraDefinitions; }; settingsFiles = [ "/var/lib/authelia-${fqdn}/oidc_clients.yaml" ]; diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix index 1a05f24..033a7ca 100644 --- a/modules/services/nextcloud-server.nix +++ b/modules/services/nextcloud-server.nix @@ -414,7 +414,6 @@ in }; }; - userGroup = lib.mkOption { type = lib.types.str; description = "Group users must belong to to be able to login to Nextcloud."; @@ -424,8 +423,8 @@ in configID = lib.mkOption { type = lib.types.int; description = '' - Multiple LDAP configs can co-exist with only one active at a time.This option - sets the config ID used by Self Host Blocks. + Multiple LDAP configs can co-exist with only one active at a time. + This option sets the config ID used by Self Host Blocks. ''; default = 50; }; @@ -475,6 +474,12 @@ in default = "one_factor"; }; + adminGroup = lib.mkOption { + type = lib.types.str; + description = "Group admins must belong to to be able to login to Nextcloud."; + default = "nextcloud_admin"; + }; + secret = lib.mkOption { description = "OIDC shared secret."; type = lib.types.submodule { @@ -1022,7 +1027,7 @@ in ${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \ 'mail;objectclass' ${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \ - 'displayname' + 'givenname' ${occ} ldap:set-config "${cID}" 'ldapUserFilter' \ '(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))' ${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \ @@ -1033,7 +1038,7 @@ in # Nextcloud is compatible with the one returned by a (possibly added in the future) SSO # provider. ${occ} ldap:set-config "${cID}" 'ldapExpertUsernameAttr' \ - 'user_id' + 'uid' ${occ} ldap:test-config -- "${cID}" @@ -1057,6 +1062,7 @@ in "profile" "email" "groups" + "nextcloud_userinfo" ]; in lib.mkIf (cfg.enable && cfg.apps.sso.enable) { assertions = [ @@ -1070,11 +1076,9 @@ in } ]; - systemd.services.nextcloud-setup.script = - '' - ${occ} app:install oidc_login || : - ${occ} app:enable oidc_login - ''; + services.nextcloud.extraApps = { + inherit (nextcloudApps) oidc_login; + }; systemd.services.nextcloud-setup-pre = { wantedBy = [ "multi-user.target" ]; @@ -1096,6 +1100,12 @@ in secretFile = "${cfg.dataDir}/config/secretFile"; # See all options at https://github.com/pulsejet/nextcloud-oidc-login + # Other important url/links are: + # ${fqdn}/.well-known/openid-configuration + # https://www.authelia.com/reference/guides/attributes/#custom-attributes + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud_oidc_authelia.md + # https://www.authelia.com/integration/openid-connect/nextcloud/#authelia + # https://www.openidconnect.net/ settings = { allow_user_to_change_display_name = false; lost_password_link = "disabled"; @@ -1110,12 +1120,14 @@ in oidc_login_logout_url = ssoFqdnWithPort; oidc_login_button_text = "Log in with ${cfg.apps.sso.provider}"; oidc_login_hide_password_form = false; - oidc_login_use_id_token = true; + # Now, Authelia provides the info using the UserInfo request. + oidc_login_use_id_token = false; oidc_login_attributes = { id = "preferred_username"; name = "name"; mail = "email"; groups = "groups"; + is_admin = "is_nextcloud_admin"; }; oidc_login_default_group = "oidc"; oidc_login_use_external_storage = false; @@ -1136,8 +1148,7 @@ in # they are not already existing, Default is `false`. This creates groups for all groups # the user is associated with in LDAP. It's too much. oidc_create_groups = false; - # Enable use of WebDAV via OIDC bearer token. - oidc_login_webdav_enabled = true; + oidc_login_webdav_enabled = false; oidc_login_password_authentication = false; oidc_login_public_key_caching_time = 86400; oidc_login_min_time_between_jwks_requests = 10; @@ -1145,19 +1156,39 @@ in # If true, nextcloud will download user avatars on login. This may lead to security issues # as the server does not control which URLs will be requested. Use with care. oidc_login_update_avatar = false; + oidc_login_code_challenge_method = "S256"; }; }; + shb.authelia.extraDefinitions = { + user_attributes."is_nextcloud_admin".expression = ''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups''; + }; + shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = { + custom_claims = { + is_nextcloud_admin = {}; + }; + }; + shb.authelia.extraOidcScopes."nextcloud_userinfo" = { + claims = [ "is_nextcloud_admin" ]; + }; + shb.authelia.oidcClients = lib.mkIf (cfg.apps.sso.provider == "Authelia") [ { client_id = cfg.apps.sso.clientID; client_name = "Nextcloud"; client_secret.source = cfg.apps.sso.secretForAuthelia.result.path; + claims_policy = "nextcloud_userinfo"; public = false; authorization_policy = cfg.apps.sso.authorization_policy; + require_pkce = "true"; + pkce_challenge_method = "S256"; redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ]; inherit scopes; - userinfo_signing_algorithm = "none"; + response_types = [ "code" ]; + grant_types = [ "authorization_code" ]; + access_token_signed_response_alg = "none"; + userinfo_signed_response_alg = "none"; + token_endpoint_auth_method = "client_secret_basic"; } ]; }) diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md index ea1ee0c..57760e9 100644 --- a/modules/services/nextcloud-server/docs/default.md +++ b/modules/services/nextcloud-server/docs/default.md @@ -179,10 +179,9 @@ to do that with SOPS we use the `key` option so that both and `sops.secrets."nextcloud/ldapUserPassword"` secrets have the same content. -Creating LDAP users and groups is not declarative yet, -so go to the LDAP server at `http://ldap.example.com`, -create the `nextcloud_user` group, -create a user and add it to the group. +The LDAP [user group](#services-nextcloudserver-options-shb.nextcloud.apps.ldap.userGroup) is created automatically. +Add your user to it by going to `http://ldap.example.com`, +create a user if needed and add it to the group. When that's done, go back to the Nextcloud server at `https://nextcloud.example.com` and login with that user. @@ -190,6 +189,7 @@ Note that we cannot create an admin user from the LDAP server, so you need to create a normal user like above, login with it once so it is known to Nextcloud, then logout, login with the admin Nextcloud user and promote that new user to admin level. +This limitation does not exist with the [SSO integration](#services-nextcloudserver-usage-oidc). ### With SSO Support {#services-nextcloudserver-usage-oidc} @@ -231,6 +231,12 @@ to do that with SOPS we use the `key` option so that both and `sops.secrets."nextcloud/sso/secretForAuthelia"` secrets have the same content. +The LDAP [user group](#services-nextcloudserver-options-shb.nextcloud.apps.ldap.userGroup) and [admin group](#services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup) are created automatically. +Add your user to one or both by going to `http://ldap.example.com`, +create a user if needed and add it to the groups. +When that's done, go back to the Nextcloud server at +`https://nextcloud.example.com` and login with that user. + Setting the `fallbackDefaultAuth` to `false` means the only way to login is through Authelia. If this does not work for any reason, you can let users login through Nextcloud directly by setting this option to `true`. diff --git a/test/common.nix b/test/common.nix index 5c5b374..8450fc6 100644 --- a/test/common.nix +++ b/test/common.nix @@ -198,6 +198,10 @@ in # HTTP(s) server port. networking.firewall.allowedTCPPorts = [ 80 443 ]; shb.nginx.accessLog = true; + + networking.hosts = { + "192.168.1.2" = [ config.test.fqdn ]; + }; }; }; @@ -415,6 +419,11 @@ in groups = [ "user_group" "admin_group" ]; password.result.path = pkgs.writeText "bobPassword" "BobPassword"; }; + # charlie = { + # email = "charlie@example.com"; + # groups = [ ]; + # password.result.path = pkgs.writeText "charliePassword" "CharliePassword"; + # }; }; ensureGroups = { @@ -430,7 +439,7 @@ in ]; networking.hosts = { - "127.0.0.1" = [ "auth.${config.test.domain}" ]; + "127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ]; }; shb.authelia = { diff --git a/test/services/nextcloud.nix b/test/services/nextcloud.nix index dad3a1f..2c46aeb 100644 --- a/test/services/nextcloud.nix +++ b/test/services/nextcloud.nix @@ -151,6 +151,91 @@ let }; }; + clientLdapLogin = { config, ... }: { + imports = [ + testLib.baseModule + testLib.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "n"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "Account name"; + passwordFieldLabelRegex = "^ *[Pp]assword"; + loginButtonNameRegex = "[Ll]og [Ii]n"; + testLoginWith = [ + { username = "alice"; password = "AlicePassword"; nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; } + { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).to_be_visible()" + ]; } + { username = "bob"; password = "BobPassword"; nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; } + { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).to_be_visible()" + ]; } + ]; + }; + }; + + clientSsoLogin = { config, ... }: { + imports = [ + testLib.baseModule + testLib.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "n"; + }; + + networking.hosts = { + "192.168.1.2" = [ "auth.example.com" ]; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldSelector = "get_by_label(\"Password *\")"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { username = "alice"; password = "AlicePassword"; nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page).to_have_title(re.compile('Dashboard'))" + "page.goto('https://${config.test.fqdn}/settings/admin')" + "expect(page.get_by_text('Access forbidden')).to_be_visible()" + ]; } + { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ + "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" + ]; } + { username = "bob"; password = "BobPassword"; nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page).to_have_title(re.compile('Dashboard'))" + "page.goto('https://${config.test.fqdn}/settings/admin')" + "expect(page.get_by_text('Access forbidden')).not_to_be_visible()" + ]; } + { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ + "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" + ]; } + # TODO: charlie has no groups, which makes lldap return a 'null' value instead of an empty array and Authelia does not like that. + # { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ + # "page.get_by_role('button', name=re.compile('Accept')).click()" + # "expect(page).to_have_title(re.compile('Dashboard'))" + # ]; } + ]; + }; + }; + https = { config, ...}: { shb.nextcloud = { ssl = config.shb.certs.certs.selfsigned.n; @@ -168,7 +253,7 @@ let dcdomain = config.shb.lldap.dcdomain; adminName = "admin"; adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result; - userGroup = "nextcloud_user"; + userGroup = "user_group"; }; }; shb.hardcodedsecret.nextcloudLdapUserPassword = { @@ -184,7 +269,7 @@ let enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; clientID = "nextcloud"; - # adminUserGroup = "nextcloud_admin"; + adminGroup = "admin_group"; secret.result = config.shb.hardcodedsecret.oidcSecret.result; secretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; @@ -192,6 +277,10 @@ let fallbackDefaultAuth = false; }; }; + # Needed because OIDC somehow does not like self-signed certificates + # which we do use in tests. + # See https://github.com/pulsejet/nextcloud-oidc-login/issues/267 + services.nextcloud.settings.oidc_login_tls_verify = lib.mkForce false; shb.hardcodedsecret.oidcSecret = { request = config.shb.nextcloud.apps.sso.secret.request; @@ -436,13 +525,19 @@ in ]; }; - nodes.client = {}; + nodes.client = { + imports = [ + clientLdapLogin + ]; + }; testScript = commonTestScript.access; }; sso = pkgs.testers.runNixOSTest { name = "nextcloud_sso"; + + interactive.sshBackdoor.enable = true; nodes.server = { config, ... }: { imports = [ @@ -453,10 +548,24 @@ in ldap (testLib.sso config.shb.certs.certs.selfsigned.n) sso + ({ config, ... }: { + networking.hosts = { + "127.0.0.1" = [ config.test.fqdn ]; + }; + }) ]; }; - nodes.client = {}; + nodes.client = { + imports = [ + clientSsoLogin + ({ config, ... }: { + networking.hosts = { + "192.168.1.2" = [ config.test.fqdn ]; + }; + }) + ]; + }; testScript = commonTestScript.access; };