commit
cf2d180a12
@ -39,7 +39,19 @@
|
||||
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para />
|
||||
<para>
|
||||
<link xlink:href="https://www.keycloak.org/">Keycloak</link>,
|
||||
an open source identity and access management server with
|
||||
support for <link
|
||||
xlink:href="https://openid.net/connect/">OpenID Connect</link>,
|
||||
<link xlink:href="https://oauth.net/2/">OAUTH 2.0</link> and
|
||||
<link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
|
||||
2.0</link>.
|
||||
</para>
|
||||
<para>
|
||||
See the <link linkend="module-services-keycloak">Keycloak
|
||||
section of the NixOS manual</link> for more information.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
||||
|
@ -863,6 +863,7 @@
|
||||
./services/web-apps/ihatemoney
|
||||
./services/web-apps/jirafeau.nix
|
||||
./services/web-apps/jitsi-meet.nix
|
||||
./services/web-apps/keycloak.nix
|
||||
./services/web-apps/limesurvey.nix
|
||||
./services/web-apps/mattermost.nix
|
||||
./services/web-apps/mediawiki.nix
|
||||
|
692
nixos/modules/services/web-apps/keycloak.nix
Normal file
692
nixos/modules/services/web-apps/keycloak.nix
Normal file
@ -0,0 +1,692 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.keycloak;
|
||||
in
|
||||
{
|
||||
options.services.keycloak = {
|
||||
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether to enable the Keycloak identity and access management
|
||||
server.
|
||||
'';
|
||||
};
|
||||
|
||||
bindAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "\${jboss.bind.address:0.0.0.0}";
|
||||
example = "127.0.0.1";
|
||||
description = ''
|
||||
On which address Keycloak should accept new connections.
|
||||
|
||||
A special syntax can be used to allow command line Java system
|
||||
properties to override the value: ''${property.name:value}
|
||||
'';
|
||||
};
|
||||
|
||||
httpPort = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "\${jboss.http.port:80}";
|
||||
example = "8080";
|
||||
description = ''
|
||||
On which port Keycloak should listen for new HTTP connections.
|
||||
|
||||
A special syntax can be used to allow command line Java system
|
||||
properties to override the value: ''${property.name:value}
|
||||
'';
|
||||
};
|
||||
|
||||
httpsPort = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "\${jboss.https.port:443}";
|
||||
example = "8443";
|
||||
description = ''
|
||||
On which port Keycloak should listen for new HTTPS connections.
|
||||
|
||||
A special syntax can be used to allow command line Java system
|
||||
properties to override the value: ''${property.name:value}
|
||||
'';
|
||||
};
|
||||
|
||||
frontendUrl = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "keycloak.example.com/auth";
|
||||
description = ''
|
||||
The public URL used as base for all frontend requests. Should
|
||||
normally include a trailing <literal>/auth</literal>.
|
||||
|
||||
See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
|
||||
Hostname section of the Keycloak server installation
|
||||
manual</link> for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
forceBackendUrlToFrontendUrl = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether Keycloak should force all requests to go through the
|
||||
frontend URL configured in <xref
|
||||
linkend="opt-services.keycloak.frontendUrl" />. By default,
|
||||
Keycloak allows backend requests to instead use its local
|
||||
hostname or IP address and may also advertise it to clients
|
||||
through its OpenID Connect Discovery endpoint.
|
||||
|
||||
See <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
|
||||
Hostname section of the Keycloak server installation
|
||||
manual</link> for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
certificatePrivateKeyBundle = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/ssl_cert";
|
||||
description = ''
|
||||
The path to a PEM formatted bundle of the private key and
|
||||
certificate to use for TLS connections.
|
||||
|
||||
This should be a string, not a Nix path, since Nix paths are
|
||||
copied into the world-readable Nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseType = lib.mkOption {
|
||||
type = lib.types.enum [ "mysql" "postgresql" ];
|
||||
default = "postgresql";
|
||||
example = "mysql";
|
||||
description = ''
|
||||
The type of database Keycloak should connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseHost = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Hostname of the database to connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
databasePort =
|
||||
let
|
||||
dbPorts = {
|
||||
postgresql = 5432;
|
||||
mysql = 3306;
|
||||
};
|
||||
in
|
||||
lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = dbPorts.${cfg.databaseType};
|
||||
description = ''
|
||||
Port of the database to connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseUseSSL = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.databaseHost != "localhost";
|
||||
description = ''
|
||||
Whether the database connection should be secured by SSL /
|
||||
TLS.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseCaCert = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The SSL / TLS CA certificate that verifies the identity of the
|
||||
database server.
|
||||
|
||||
Required when PostgreSQL is used and SSL is turned on.
|
||||
|
||||
For MySQL, if left at <literal>null</literal>, the default
|
||||
Java keystore is used, which should suffice if the server
|
||||
certificate is issued by an official CA.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseCreateLocally = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether a database should be automatically created on the
|
||||
local host. Set this to false if you plan on provisioning a
|
||||
local database yourself. This has no effect if
|
||||
services.keycloak.databaseHost is customized.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseUsername = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "keycloak";
|
||||
description = ''
|
||||
Username to use when connecting to an external or manually
|
||||
provisioned database; has no effect when a local database is
|
||||
automatically provisioned.
|
||||
'';
|
||||
};
|
||||
|
||||
databasePasswordFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
example = "/run/keys/db_password";
|
||||
description = ''
|
||||
File containing the database password.
|
||||
|
||||
This should be a string, not a Nix path, since Nix paths are
|
||||
copied into the world-readable Nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.keycloak;
|
||||
description = ''
|
||||
Keycloak package to use.
|
||||
'';
|
||||
};
|
||||
|
||||
initialAdminPassword = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "changeme";
|
||||
description = ''
|
||||
Initial password set for the <literal>admin</literal>
|
||||
user. The password is not stored safely and should be changed
|
||||
immediately in the admin panel.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
example = lib.literalExample ''
|
||||
{
|
||||
"subsystem=keycloak-server" = {
|
||||
"spi=hostname" = {
|
||||
"provider=default" = null;
|
||||
"provider=fixed" = {
|
||||
enabled = true;
|
||||
properties.hostname = "keycloak.example.com";
|
||||
};
|
||||
default-provider = "fixed";
|
||||
};
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Additional Keycloak configuration options to set in
|
||||
<literal>standalone.xml</literal>.
|
||||
|
||||
Options are expressed as a Nix attribute set which matches the
|
||||
structure of the jboss-cli configuration. The configuration is
|
||||
effectively overlayed on top of the default configuration
|
||||
shipped with Keycloak. To remove existing nodes and undefine
|
||||
attributes from the default configuration, set them to
|
||||
<literal>null</literal>.
|
||||
|
||||
The example configuration does the equivalent of the following
|
||||
script, which removes the hostname provider
|
||||
<literal>default</literal>, adds the deprecated hostname
|
||||
provider <literal>fixed</literal> and defines it the default:
|
||||
|
||||
<programlisting>
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:remove()
|
||||
/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
|
||||
/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
|
||||
</programlisting>
|
||||
|
||||
You can discover available options by using the <link
|
||||
xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
|
||||
program and by referring to the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
|
||||
Server Installation and Configuration Guide</link>.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
# We only want to create a database if we're actually going to connect to it.
|
||||
databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "localhost";
|
||||
createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.databaseType == "postgresql";
|
||||
createLocalMySQL = databaseActuallyCreateLocally && cfg.databaseType == "mysql";
|
||||
|
||||
mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} ''
|
||||
${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt
|
||||
'';
|
||||
|
||||
keycloakConfig' = builtins.foldl' lib.recursiveUpdate {
|
||||
"interface=public".inet-address = cfg.bindAddress;
|
||||
"socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
|
||||
"subsystem=keycloak-server"."spi=hostname" = {
|
||||
"provider=default" = {
|
||||
enabled = true;
|
||||
properties = {
|
||||
inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
|
||||
};
|
||||
};
|
||||
};
|
||||
"subsystem=datasources"."data-source=KeycloakDS" = {
|
||||
max-pool-size = "20";
|
||||
user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername;
|
||||
password = "@db-password@";
|
||||
};
|
||||
} [
|
||||
(lib.optionalAttrs (cfg.databaseType == "postgresql") {
|
||||
"subsystem=datasources" = {
|
||||
"jdbc-driver=postgresql" = {
|
||||
driver-module-name = "org.postgresql";
|
||||
driver-name = "postgresql";
|
||||
driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource";
|
||||
};
|
||||
"data-source=KeycloakDS" = {
|
||||
connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak";
|
||||
driver-name = "postgresql";
|
||||
"connection-properties=ssl".value = lib.boolToString cfg.databaseUseSSL;
|
||||
} // (lib.optionalAttrs (cfg.databaseCaCert != null) {
|
||||
"connection-properties=sslrootcert".value = cfg.databaseCaCert;
|
||||
"connection-properties=sslmode".value = "verify-ca";
|
||||
});
|
||||
};
|
||||
})
|
||||
(lib.optionalAttrs (cfg.databaseType == "mysql") {
|
||||
"subsystem=datasources" = {
|
||||
"jdbc-driver=mysql" = {
|
||||
driver-module-name = "com.mysql";
|
||||
driver-name = "mysql";
|
||||
driver-class-name = "com.mysql.jdbc.Driver";
|
||||
};
|
||||
"data-source=KeycloakDS" = {
|
||||
connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak";
|
||||
driver-name = "mysql";
|
||||
"connection-properties=useSSL".value = lib.boolToString cfg.databaseUseSSL;
|
||||
"connection-properties=requireSSL".value = lib.boolToString cfg.databaseUseSSL;
|
||||
"connection-properties=verifyServerCertificate".value = lib.boolToString cfg.databaseUseSSL;
|
||||
"connection-properties=characterEncoding".value = "UTF-8";
|
||||
valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
|
||||
validate-on-match = true;
|
||||
exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
|
||||
} // (lib.optionalAttrs (cfg.databaseCaCert != null) {
|
||||
"connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
|
||||
"connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
|
||||
});
|
||||
};
|
||||
})
|
||||
(lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) {
|
||||
"socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
|
||||
"core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = {
|
||||
keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
|
||||
keystore-password = "notsosecretpassword";
|
||||
};
|
||||
"subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm";
|
||||
})
|
||||
cfg.extraConfig
|
||||
];
|
||||
|
||||
|
||||
/* Produces a JBoss CLI script that creates paths and sets
|
||||
attributes matching those described by `attrs`. When the
|
||||
script is run, the existing settings are effectively overlayed
|
||||
by those from `attrs`. Existing attributes can be unset by
|
||||
defining them `null`.
|
||||
|
||||
JBoss paths and attributes / maps are distinguished by their
|
||||
name, where paths follow a `key=value` scheme.
|
||||
|
||||
Example:
|
||||
mkJbossScript {
|
||||
"subsystem=keycloak-server"."spi=hostname" = {
|
||||
"provider=fixed" = null;
|
||||
"provider=default" = {
|
||||
enabled = true;
|
||||
properties = {
|
||||
inherit frontendUrl;
|
||||
forceBackendUrlToFrontendUrl = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
=> ''
|
||||
if (outcome != success) of /:read-resource()
|
||||
/:add()
|
||||
end-if
|
||||
if (outcome != success) of /subsystem=keycloak-server:read-resource()
|
||||
/subsystem=keycloak-server:add()
|
||||
end-if
|
||||
if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
|
||||
/subsystem=keycloak-server/spi=hostname:add()
|
||||
end-if
|
||||
if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
|
||||
end-if
|
||||
if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
|
||||
end-if
|
||||
if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
|
||||
end-if
|
||||
if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
|
||||
end-if
|
||||
if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
|
||||
/subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
|
||||
end-if
|
||||
''
|
||||
*/
|
||||
mkJbossScript = attrs:
|
||||
let
|
||||
/* From a JBoss path and an attrset, produces a JBoss CLI
|
||||
snippet that writes the corresponding attributes starting
|
||||
at `path`. Recurses down into subattrsets as necessary,
|
||||
producing the variable name from its full path in the
|
||||
attrset.
|
||||
|
||||
Example:
|
||||
writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
|
||||
enabled = true;
|
||||
properties = {
|
||||
forceBackendUrlToFrontendUrl = false;
|
||||
frontendUrl = "https://keycloak.example.com/auth";
|
||||
};
|
||||
}
|
||||
=> ''
|
||||
if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
|
||||
end-if
|
||||
if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
|
||||
end-if
|
||||
if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
|
||||
end-if
|
||||
''
|
||||
*/
|
||||
writeAttributes = path: set:
|
||||
let
|
||||
# JBoss expressions like `${var}` need to be prefixed
|
||||
# with `expression` to evaluate.
|
||||
prefixExpression = string:
|
||||
let
|
||||
match = (builtins.match ''"\$\{.*}"'' string);
|
||||
in
|
||||
if match != null then
|
||||
"expression " + string
|
||||
else
|
||||
string;
|
||||
|
||||
writeAttribute = attribute: value:
|
||||
let
|
||||
type = builtins.typeOf value;
|
||||
in
|
||||
if type == "set" then
|
||||
let
|
||||
names = builtins.attrNames value;
|
||||
in
|
||||
builtins.foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
|
||||
else if value == null then ''
|
||||
if (outcome == success) of ${path}:read-attribute(name="${attribute}")
|
||||
${path}:undefine-attribute(name="${attribute}")
|
||||
end-if
|
||||
''
|
||||
else if builtins.elem type [ "string" "path" "bool" ] then
|
||||
let
|
||||
value' = if type == "bool" then lib.boolToString value else ''"${value}"'';
|
||||
in ''
|
||||
if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
|
||||
${path}:write-attribute(name=${attribute}, value=${value'})
|
||||
end-if
|
||||
''
|
||||
else throw "Unsupported type '${type}' for path '${path}'!";
|
||||
in
|
||||
lib.concatStrings
|
||||
(lib.mapAttrsToList
|
||||
(attribute: value: (writeAttribute attribute value))
|
||||
set);
|
||||
|
||||
|
||||
/* Produces an argument list for the JBoss `add()` function,
|
||||
which adds a JBoss path and takes as its arguments the
|
||||
required subpaths and attributes.
|
||||
|
||||
Example:
|
||||
makeArgList {
|
||||
enabled = true;
|
||||
properties = {
|
||||
forceBackendUrlToFrontendUrl = false;
|
||||
frontendUrl = "https://keycloak.example.com/auth";
|
||||
};
|
||||
}
|
||||
=> ''
|
||||
enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
|
||||
''
|
||||
*/
|
||||
makeArgList = set:
|
||||
let
|
||||
makeArg = attribute: value:
|
||||
let
|
||||
type = builtins.typeOf value;
|
||||
in
|
||||
if type == "set" then
|
||||
"${attribute} = { " + (makeArgList value) + " }"
|
||||
else if builtins.elem type [ "string" "path" "bool" ] then
|
||||
"${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}"
|
||||
else if value == null then
|
||||
""
|
||||
else
|
||||
throw "Unsupported type '${type}' for attribute '${attribute}'!";
|
||||
in
|
||||
lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set);
|
||||
|
||||
|
||||
/* Recurses into the `attrs` attrset, beginning at the path
|
||||
resolved from `state.path ++ node`; if `node` is `null`,
|
||||
starts from `state.path`. Only subattrsets that are JBoss
|
||||
paths, i.e. follows the `key=value` format, are recursed
|
||||
into - the rest are considered JBoss attributes / maps.
|
||||
*/
|
||||
recurse = state: node:
|
||||
let
|
||||
path = state.path ++ (lib.optional (node != null) node);
|
||||
isPath = name:
|
||||
let
|
||||
value = lib.getAttrFromPath (path ++ [ name ]) attrs;
|
||||
in
|
||||
if (builtins.match ".*([=]).*" name) == [ "=" ] then
|
||||
if builtins.isAttrs value || value == null then
|
||||
true
|
||||
else
|
||||
throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
|
||||
else
|
||||
false;
|
||||
jbossPath = "/" + (lib.concatStringsSep "/" path);
|
||||
nodeValue = lib.getAttrFromPath path attrs;
|
||||
children = if !builtins.isAttrs nodeValue then {} else nodeValue;
|
||||
subPaths = builtins.filter isPath (builtins.attrNames children);
|
||||
jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children;
|
||||
in
|
||||
state // {
|
||||
text = state.text + (
|
||||
if nodeValue != null then ''
|
||||
if (outcome != success) of ${jbossPath}:read-resource()
|
||||
${jbossPath}:add(${makeArgList jbossAttrs})
|
||||
end-if
|
||||
'' + (writeAttributes jbossPath jbossAttrs)
|
||||
else ''
|
||||
if (outcome == success) of ${jbossPath}:read-resource()
|
||||
${jbossPath}:remove()
|
||||
end-if
|
||||
'') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text;
|
||||
};
|
||||
in
|
||||
(recurse { text = ""; path = []; } null).text;
|
||||
|
||||
|
||||
jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
|
||||
|
||||
keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} ''
|
||||
export JBOSS_BASE_DIR="$(pwd -P)";
|
||||
export JBOSS_MODULEPATH="${cfg.package}/modules";
|
||||
export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log";
|
||||
|
||||
cp -r ${cfg.package}/standalone/configuration .
|
||||
chmod -R u+rwX ./configuration
|
||||
|
||||
mkdir -p {deployments,ssl}
|
||||
|
||||
"${cfg.package}/bin/standalone.sh"&
|
||||
|
||||
attempt=1
|
||||
max_attempts=30
|
||||
while ! ${cfg.package}/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'; do
|
||||
if [[ "$attempt" == "$max_attempts" ]]; then
|
||||
echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
|
||||
sleep 1
|
||||
(( attempt++ ))
|
||||
done
|
||||
|
||||
${cfg.package}/bin/jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
|
||||
|
||||
cp configuration/standalone.xml $out
|
||||
'';
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = (cfg.databaseUseSSL && cfg.databaseType == "postgresql") -> (cfg.databaseCaCert != null);
|
||||
message = ''A CA certificate must be specified (in 'services.keycloak.databaseCaCert') when PostgreSQL is used with SSL'';
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL {
|
||||
after = [ "postgresql.service" ];
|
||||
before = [ "keycloak.service" ];
|
||||
bindsTo = [ "postgresql.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = "postgres";
|
||||
Group = "postgres";
|
||||
};
|
||||
script = ''
|
||||
set -eu
|
||||
|
||||
PSQL=${config.services.postgresql.package}/bin/psql
|
||||
|
||||
db_password="$(<'${cfg.databasePasswordFile}')"
|
||||
$PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tAc "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB"
|
||||
$PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL {
|
||||
after = [ "mysql.service" ];
|
||||
before = [ "keycloak.service" ];
|
||||
bindsTo = [ "mysql.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = config.services.mysql.user;
|
||||
Group = config.services.mysql.group;
|
||||
};
|
||||
script = ''
|
||||
set -eu
|
||||
|
||||
db_password="$(<'${cfg.databasePasswordFile}')"
|
||||
( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
|
||||
echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
|
||||
echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
|
||||
) | ${config.services.mysql.package}/bin/mysql -N
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.keycloak =
|
||||
let
|
||||
databaseServices =
|
||||
if createLocalPostgreSQL then [
|
||||
"keycloakPostgreSQLInit.service" "postgresql.service"
|
||||
]
|
||||
else if createLocalMySQL then [
|
||||
"keycloakMySQLInit.service" "mysql.service"
|
||||
]
|
||||
else [ ];
|
||||
in {
|
||||
after = databaseServices;
|
||||
bindsTo = databaseServices;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
JBOSS_LOG_DIR = "/var/log/keycloak";
|
||||
JBOSS_BASE_DIR = "/run/keycloak";
|
||||
JBOSS_MODULEPATH = "${cfg.package}/modules";
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStartPre = let
|
||||
startPreFullPrivileges = ''
|
||||
set -eu
|
||||
|
||||
install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password
|
||||
'' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) ''
|
||||
install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle
|
||||
'';
|
||||
startPre = ''
|
||||
set -eu
|
||||
|
||||
install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
|
||||
install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
|
||||
|
||||
db_password="$(</run/keycloak/secrets/db_password)"
|
||||
${pkgs.replace}/bin/replace-literal -fe '@db-password@' "$db_password" /run/keycloak/configuration/standalone.xml
|
||||
|
||||
export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
|
||||
${cfg.package}/bin/add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
|
||||
'' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) ''
|
||||
pushd /run/keycloak/ssl/
|
||||
cat /run/keycloak/secrets/ssl_cert_pk_bundle <(echo) /etc/ssl/certs/ca-certificates.crt > allcerts.pem
|
||||
${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain \
|
||||
-name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
|
||||
-CAfile allcerts.pem -passout pass:notsosecretpassword
|
||||
popd
|
||||
'';
|
||||
in [
|
||||
"+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}"
|
||||
"${pkgs.writeShellScript "keycloak-start-pre" startPre}"
|
||||
];
|
||||
ExecStart = "${cfg.package}/bin/standalone.sh";
|
||||
User = "keycloak";
|
||||
Group = "keycloak";
|
||||
DynamicUser = true;
|
||||
RuntimeDirectory = map (p: "keycloak/" + p) [
|
||||
"secrets"
|
||||
"configuration"
|
||||
"deployments"
|
||||
"data"
|
||||
"ssl"
|
||||
"log"
|
||||
"tmp"
|
||||
];
|
||||
RuntimeDirectoryMode = 0700;
|
||||
LogsDirectory = "keycloak";
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
};
|
||||
};
|
||||
|
||||
services.postgresql.enable = lib.mkDefault createLocalPostgreSQL;
|
||||
services.mysql.enable = lib.mkDefault createLocalMySQL;
|
||||
services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql;
|
||||
};
|
||||
|
||||
meta.doc = ./keycloak.xml;
|
||||
}
|
205
nixos/modules/services/web-apps/keycloak.xml
Normal file
205
nixos/modules/services/web-apps/keycloak.xml
Normal file
@ -0,0 +1,205 @@
|
||||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-keycloak">
|
||||
<title>Keycloak</title>
|
||||
<para>
|
||||
<link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
|
||||
open source identity and access management server with support for
|
||||
<link xlink:href="https://openid.net/connect/">OpenID
|
||||
Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
|
||||
2.0</link> and <link
|
||||
xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
|
||||
2.0</link>.
|
||||
</para>
|
||||
<section xml:id="module-services-keycloak-admin">
|
||||
<title>Administration</title>
|
||||
<para>
|
||||
An administrative user with the username
|
||||
<literal>admin</literal> is automatically created in the
|
||||
<literal>master</literal> realm. Its initial password can be
|
||||
configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
|
||||
and defaults to <literal>changeme</literal>. The password is
|
||||
not stored safely and should be changed immediately in the
|
||||
admin panel.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Refer to the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
|
||||
Console section of the Keycloak Server Administration Guide</link> for
|
||||
information on how to administer your
|
||||
<productname>Keycloak</productname> instance.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-database">
|
||||
<title>Database access</title>
|
||||
<para>
|
||||
<productname>Keycloak</productname> can be used with either
|
||||
<productname>PostgreSQL</productname> or
|
||||
<productname>MySQL</productname>. Which one is used can be
|
||||
configured in <xref
|
||||
linkend="opt-services.keycloak.databaseType" />. The selected
|
||||
database will automatically be enabled and a database and role
|
||||
created unless <xref
|
||||
linkend="opt-services.keycloak.databaseHost" /> is changed from
|
||||
its default of <literal>localhost</literal> or <xref
|
||||
linkend="opt-services.keycloak.databaseCreateLocally" /> is set
|
||||
to <literal>false</literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
External database access can also be configured by setting
|
||||
<xref linkend="opt-services.keycloak.databaseHost" />, <xref
|
||||
linkend="opt-services.keycloak.databaseUsername" />, <xref
|
||||
linkend="opt-services.keycloak.databaseUseSSL" /> and <xref
|
||||
linkend="opt-services.keycloak.databaseCaCert" /> as
|
||||
appropriate. Note that you need to manually create a database
|
||||
called <literal>keycloak</literal> and allow the configured
|
||||
database user full access to it.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
<xref linkend="opt-services.keycloak.databasePasswordFile" />
|
||||
must be set to the path to a file containing the password used
|
||||
to log in to the database. If <xref linkend="opt-services.keycloak.databaseHost" />
|
||||
and <xref linkend="opt-services.keycloak.databaseCreateLocally" />
|
||||
are kept at their defaults, the database role
|
||||
<literal>keycloak</literal> with that password is provisioned
|
||||
on the local database instance.
|
||||
</para>
|
||||
|
||||
<warning>
|
||||
<para>
|
||||
The path should be provided as a string, not a Nix path, since Nix
|
||||
paths are copied into the world readable Nix store.
|
||||
</para>
|
||||
</warning>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-frontendurl">
|
||||
<title>Frontend URL</title>
|
||||
<para>
|
||||
The frontend URL is used as base for all frontend requests and
|
||||
must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
|
||||
It should normally include a trailing <literal>/auth</literal>
|
||||
(the default web context).
|
||||
</para>
|
||||
|
||||
<para>
|
||||
<xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
|
||||
determines whether Keycloak should force all requests to go
|
||||
through the frontend URL. By default,
|
||||
<productname>Keycloak</productname> allows backend requests to
|
||||
instead use its local hostname or IP address and may also
|
||||
advertise it to clients through its OpenID Connect Discovery
|
||||
endpoint.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
See the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
|
||||
section of the Keycloak Server Installation and Configuration
|
||||
Guide</link> for more information.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-tls">
|
||||
<title>Setting up TLS/SSL</title>
|
||||
<para>
|
||||
By default, <productname>Keycloak</productname> won't accept
|
||||
unsecured HTTP connections originating from outside its local
|
||||
network.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
For HTTPS support, a TLS certificate and private key is
|
||||
required. They should be <link
|
||||
xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
|
||||
formatted</link> and concatenated into a single file. The path
|
||||
to this file should be configured in
|
||||
<xref linkend="opt-services.keycloak.certificatePrivateKeyBundle" />.
|
||||
</para>
|
||||
|
||||
<warning>
|
||||
<para>
|
||||
The path should be provided as a string, not a Nix path,
|
||||
since Nix paths are copied into the world readable Nix store.
|
||||
</para>
|
||||
</warning>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-extra-config">
|
||||
<title>Additional configuration</title>
|
||||
<para>
|
||||
Additional Keycloak configuration options, for which no
|
||||
explicit <productname>NixOS</productname> options are provided,
|
||||
can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Options are expressed as a Nix attribute set which matches the
|
||||
structure of the jboss-cli configuration. The configuration is
|
||||
effectively overlayed on top of the default configuration
|
||||
shipped with Keycloak. To remove existing nodes and undefine
|
||||
attributes from the default configuration, set them to
|
||||
<literal>null</literal>.
|
||||
</para>
|
||||
<para>
|
||||
For example, the following script, which removes the hostname
|
||||
provider <literal>default</literal>, adds the deprecated
|
||||
hostname provider <literal>fixed</literal> and defines it the
|
||||
default:
|
||||
|
||||
<programlisting>
|
||||
/subsystem=keycloak-server/spi=hostname/provider=default:remove()
|
||||
/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
|
||||
/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
|
||||
</programlisting>
|
||||
|
||||
would be expressed as
|
||||
|
||||
<programlisting>
|
||||
services.keycloak.extraConfig = {
|
||||
"subsystem=keycloak-server" = {
|
||||
"spi=hostname" = {
|
||||
"provider=default" = null;
|
||||
"provider=fixed" = {
|
||||
enabled = true;
|
||||
properties.hostname = "keycloak.example.com";
|
||||
};
|
||||
default-provider = "fixed";
|
||||
};
|
||||
};
|
||||
};
|
||||
</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
You can discover available options by using the <link
|
||||
xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
|
||||
program and by referring to the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
|
||||
Server Installation and Configuration Guide</link>.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-example-config">
|
||||
<title>Example configuration</title>
|
||||
<para>
|
||||
A basic configuration with some custom settings could look like this:
|
||||
<programlisting>
|
||||
services.keycloak = {
|
||||
<link linkend="opt-services.keycloak.enable">enable</link> = true;
|
||||
<link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl"; # change on first login
|
||||
<link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
|
||||
<link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
|
||||
<link linkend="opt-services.keycloak.certificatePrivateKeyBundle">certificatePrivateKeyBundle</link> = "/run/keys/ssl_cert";
|
||||
<link linkend="opt-services.keycloak.databasePasswordFile">databasePasswordFile</link> = "/run/keys/db_password";
|
||||
};
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
</section>
|
||||
</chapter>
|
@ -175,6 +175,7 @@ in
|
||||
kernel-latest = handleTest ./kernel-latest.nix {};
|
||||
kernel-lts = handleTest ./kernel-lts.nix {};
|
||||
kernel-testing = handleTest ./kernel-testing.nix {};
|
||||
keycloak = discoverTests (import ./keycloak.nix);
|
||||
keymap = handleTest ./keymap.nix {};
|
||||
knot = handleTest ./knot.nix {};
|
||||
krb5 = discoverTests (import ./krb5 {});
|
||||
|
144
nixos/tests/keycloak.nix
Normal file
144
nixos/tests/keycloak.nix
Normal file
@ -0,0 +1,144 @@
|
||||
# This tests Keycloak: it starts the service, creates a realm with an
|
||||
# OIDC client and a user, and simulates the user logging in to the
|
||||
# client using their Keycloak login.
|
||||
|
||||
let
|
||||
frontendUrl = "http://keycloak/auth";
|
||||
initialAdminPassword = "h4IhoJFnt2iQIR9";
|
||||
|
||||
keycloakTest = import ./make-test-python.nix (
|
||||
{ pkgs, databaseType, ... }:
|
||||
{
|
||||
name = "keycloak";
|
||||
meta = with pkgs.stdenv.lib.maintainers; {
|
||||
maintainers = [ talyz ];
|
||||
};
|
||||
|
||||
nodes = {
|
||||
keycloak = { ... }: {
|
||||
virtualisation.memorySize = 1024;
|
||||
services.keycloak = {
|
||||
enable = true;
|
||||
inherit frontendUrl databaseType initialAdminPassword;
|
||||
databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
|
||||
};
|
||||
environment.systemPackages = with pkgs; [
|
||||
xmlstarlet
|
||||
libtidy
|
||||
jq
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
let
|
||||
client = {
|
||||
clientId = "test-client";
|
||||
name = "test-client";
|
||||
redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
|
||||
};
|
||||
|
||||
user = {
|
||||
firstName = "Chuck";
|
||||
lastName = "Testa";
|
||||
username = "chuck.testa";
|
||||
email = "chuck.testa@example.com";
|
||||
};
|
||||
|
||||
password = "password1234";
|
||||
|
||||
realm = {
|
||||
enabled = true;
|
||||
realm = "test-realm";
|
||||
clients = [ client ];
|
||||
users = [(
|
||||
user // {
|
||||
enabled = true;
|
||||
credentials = [{
|
||||
type = "password";
|
||||
temporary = false;
|
||||
value = password;
|
||||
}];
|
||||
}
|
||||
)];
|
||||
};
|
||||
|
||||
realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
|
||||
|
||||
jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
|
||||
if {
|
||||
"firstName": .given_name,
|
||||
"lastName": .family_name,
|
||||
"username": .preferred_username,
|
||||
"email": .email
|
||||
} != ${builtins.toJSON user} then
|
||||
error("Wrong user info!")
|
||||
else
|
||||
empty
|
||||
end
|
||||
'';
|
||||
in ''
|
||||
keycloak.start()
|
||||
keycloak.wait_for_unit("keycloak.service")
|
||||
keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
|
||||
|
||||
|
||||
### Realm Setup ###
|
||||
|
||||
# Get an admin interface access token
|
||||
keycloak.succeed(
|
||||
"curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >admin_auth_header"
|
||||
)
|
||||
|
||||
# Publish the realm, including a test OIDC client and user
|
||||
keycloak.succeed(
|
||||
"curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
|
||||
)
|
||||
|
||||
# Generate and save the client secret. To do this we need
|
||||
# Keycloak's internal id for the client.
|
||||
keycloak.succeed(
|
||||
"curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
|
||||
"curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
|
||||
)
|
||||
|
||||
|
||||
### Authentication Testing ###
|
||||
|
||||
# Start the login process by sending an initial request to the
|
||||
# OIDC authentication endpoint, saving the returned page. Tidy
|
||||
# up the HTML (XmlStarlet is picky) and extract the login form
|
||||
# post url.
|
||||
keycloak.succeed(
|
||||
"curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form",
|
||||
"tidy -q -m login_form || true",
|
||||
"xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']\" -v @action login_form >form_post_url",
|
||||
)
|
||||
|
||||
# Post the login form and save the response. Once again tidy up
|
||||
# the HTML, then extract the authorization code.
|
||||
keycloak.succeed(
|
||||
"curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
|
||||
"tidy -q -m auth_code_html || true",
|
||||
"xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
|
||||
)
|
||||
|
||||
# Exchange the authorization code for an access token.
|
||||
keycloak.succeed(
|
||||
"curl -sSf -d grant_type=authorization_code -d code=$(<auth_code) -d client_id=${client.name} -d client_secret=$(<client_secret) -d redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >auth_header"
|
||||
)
|
||||
|
||||
# Use the access token on the OIDC userinfo endpoint and check
|
||||
# that the returned user info matches what we initialized the
|
||||
# realm with.
|
||||
keycloak.succeed(
|
||||
"curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
|
||||
)
|
||||
'';
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
postgres = keycloakTest { databaseType = "postgresql"; };
|
||||
mysql = keycloakTest { databaseType = "mysql"; };
|
||||
}
|
@ -1,5 +1,21 @@
|
||||
{ stdenv, fetchzip, makeWrapper, jre }:
|
||||
{ stdenv, lib, fetchzip, makeWrapper, jre, writeText, nixosTests
|
||||
, postgresql_jdbc ? null, mysql_jdbc ? null
|
||||
}:
|
||||
|
||||
let
|
||||
mkModuleXml = name: jarFile: writeText "module.xml" ''
|
||||
<?xml version="1.0" ?>
|
||||
<module xmlns="urn:jboss:module:1.3" name="${name}">
|
||||
<resources>
|
||||
<resource-root path="${jarFile}"/>
|
||||
</resources>
|
||||
<dependencies>
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.transaction.api"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
'';
|
||||
in
|
||||
stdenv.mkDerivation rec {
|
||||
pname = "keycloak";
|
||||
version = "11.0.2";
|
||||
@ -16,20 +32,37 @@ stdenv.mkDerivation rec {
|
||||
cp -r * $out
|
||||
|
||||
rm -rf $out/bin/*.{ps1,bat}
|
||||
rm -rf $out/bin/add-user-keycloak.sh
|
||||
rm -rf $out/bin/jconsole.sh
|
||||
|
||||
chmod +x $out/bin/standalone.sh
|
||||
wrapProgram $out/bin/standalone.sh \
|
||||
--prefix PATH ":" ${jre}/bin ;
|
||||
module_path=$out/modules/system/layers/keycloak
|
||||
if ! [[ -d $module_path ]]; then
|
||||
echo "The module path $module_path not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${lib.optionalString (postgresql_jdbc != null) ''
|
||||
mkdir -p $module_path/org/postgresql/main
|
||||
ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/org/postgresql/main/
|
||||
ln -s ${mkModuleXml "org.postgresql" "postgresql-jdbc.jar"} $module_path/org/postgresql/main/module.xml
|
||||
''}
|
||||
${lib.optionalString (mysql_jdbc != null) ''
|
||||
mkdir -p $module_path/com/mysql/main
|
||||
ln -s ${mysql_jdbc}/share/java/mysql-connector-java.jar $module_path/com/mysql/main/
|
||||
ln -s ${mkModuleXml "com.mysql" "mysql-connector-java.jar"} $module_path/com/mysql/main/module.xml
|
||||
''}
|
||||
|
||||
wrapProgram $out/bin/standalone.sh --set JAVA_HOME ${jre}
|
||||
wrapProgram $out/bin/add-user-keycloak.sh --set JAVA_HOME ${jre}
|
||||
wrapProgram $out/bin/jboss-cli.sh --set JAVA_HOME ${jre}
|
||||
'';
|
||||
|
||||
passthru.tests = nixosTests.keycloak;
|
||||
|
||||
meta = with stdenv.lib; {
|
||||
homepage = "https://www.keycloak.org/";
|
||||
description = "Identity and access management for modern applications and services";
|
||||
license = licenses.asl20;
|
||||
platforms = jre.meta.platforms;
|
||||
maintainers = [ maintainers.ngerstle ];
|
||||
maintainers = with maintainers; [ ngerstle talyz ];
|
||||
};
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user