Keycloak realm with groups (#2002)

* added a group and mapper to keycloak w/ burnettk

* accept an internal uri to keycloak w/ burnettk

* pyl w/ burnettk

* the only time we ever use internal arg to open_id_endpoint_for_name we want it True

* protect users of openid urls from internal urls

* allow port 8000/8001 for docker and avoid public urls when using requests again

* allow 8001 frontend in docker compose post logout redirect url

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2024-07-29 10:39:50 -04:00 committed by GitHub
parent b8c7aa991d
commit d2c5c27bdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 129 additions and 106 deletions

View File

@ -30,7 +30,7 @@ on:
push:
branches:
- main
- spiffui/newui
- keycloak-realm-with-groups
tags: [v*]
jobs:

View File

@ -1,17 +1,20 @@
#!/usr/bin/env bash
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
script_dir="$(
cd -- "$(dirname "$0")" >/dev/null 2>&1
pwd -P
)"
realms="$*"
if [[ -z "$realms" ]]; then
realms="spiffworkflow-realm"
realms="spiffworkflow-local-realm"
fi
docker_container_path=/tmp/hey
@ -20,8 +23,8 @@ docker exec keycloak rm -rf "$docker_container_path"
docker exec keycloak /opt/keycloak/bin/kc.sh export --dir "${docker_container_path}" --users realm_file || echo ''
docker cp "keycloak:${docker_container_path}" "$local_tmp_dir"
for realm in $realms ; do
if ! grep -Eq '\-realm$' <<< "$realm"; then
for realm in $realms; do
if ! grep -Eq '\-realm$' <<<"$realm"; then
realm="${realm}-realm"
fi
cp "${local_tmp_dir}/hey/${realm}.json" "${script_dir}/../realm_exports/"

View File

@ -441,7 +441,17 @@
]
}
},
"groups": [],
"groups": [
{
"id": "a14e6081-ffd4-4925-9364-4916bcf74931",
"name": "group_keycloak",
"path": "/group_keycloak",
"attributes": {},
"realmRoles": [],
"clientRoles": {},
"subGroups": []
}
],
"defaultRole": {
"id": "c9f0ff93-642d-402b-965a-04d70719886b",
"name": "default-roles-spiffworkflow",
@ -458,7 +468,11 @@
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"],
"otpSupportedApplications": [
"totpAppMicrosoftAuthenticatorName",
"totpAppFreeOTPName",
"totpAppGoogleName"
],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
"webAuthnPolicyRpId": "",
@ -493,7 +507,7 @@
"id": "644ff652-31d1-4349-9340-ce3b5fb76b5c",
"type": "password",
"createdDate": 1676302139645,
"secretData": "{\"value\":\"P6nrEJgmgdL+HbNA9tPkOyHYaLAqLg6cXh27ACgVQiOox4qwNE8Iv1L4ssispV4gsmuHiY+alio/2UMuyMGvEw==\",\"salt\":\"g214ckcAyig59U/3Oqwq4w==\",\"additionalParameters\":{}}",
"secretData": "{\"value\":\"nhHncfQqkY1vhJE8QxjD3hwpznBnZNc5+OxS79OFKJc=\",\"salt\":\"EradsPFeRiQ3oMAzGqfWBw==\",\"additionalParameters\":{}}",
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
}
],
@ -504,7 +518,7 @@
"realm-management": ["realm-admin"]
},
"notBefore": 0,
"groups": []
"groups": ["/group_keycloak"]
},
{
"id": "4c436296-8471-4105-b551-80eee96b43bb",
@ -651,6 +665,42 @@
"realmRoles": ["default-roles-spiffworkflow"],
"notBefore": 0,
"groups": []
},
{
"id": "d2a86264-e3ca-4a82-99b8-0a6cd714ded4",
"createdTimestamp": 1722019421345,
"username": "service-account-spiffworkflow-backend",
"enabled": true,
"totp": false,
"emailVerified": false,
"serviceAccountClientId": "spiffworkflow-backend",
"credentials": [],
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-spiffworkflow"],
"clientRoles": {
"spiffworkflow-backend": ["uma_protection"]
},
"notBefore": 0,
"groups": []
},
{
"id": "bae51d14-9d82-468f-9e9d-1cd2cb507f15",
"createdTimestamp": 1722019421396,
"username": "service-account-withauth",
"enabled": true,
"totp": false,
"emailVerified": false,
"serviceAccountClientId": "withAuth",
"credentials": [],
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-spiffworkflow"],
"clientRoles": {
"withAuth": ["uma_protection"]
},
"notBefore": 0,
"groups": []
}
],
"scopeMappings": [
@ -1031,9 +1081,8 @@
"secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",
"redirectUris": [
"http://localhost:7000/*",
"https://replace-me-with-spiff-backend-host-and-path/*",
"http://67.205.133.116:7000/*",
"http://167.172.242.138:7000/*"
"http://localhost:8000/*",
"https://for-local-dev.spiffworkflow.org/*"
],
"webOrigins": [],
"notBefore": 0,
@ -1051,7 +1100,7 @@
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"frontchannel.logout.session.required": "false",
"post.logout.redirect.uris": "https://replace-me-with-spiff-frontend-host-and-path/*##http://localhost:7001/*",
"post.logout.redirect.uris": "https://replace-me-with-spiff-frontend-host-and-path/*##http://localhost:7001/*##http://localhost:8001/*",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature.keyinfo.ext": "false",
@ -1320,9 +1369,7 @@
"redirectUris": [
"https://api.unused-for-local-dev.spiffworkflow.org/*",
"http://localhost:7001/*",
"http://67.205.133.116:7000/*",
"http://167.172.242.138:7001/*",
"https://api.demo.spiffworkflow.org/*"
"http://localhost:8001/*"
],
"webOrigins": ["*"],
"notBefore": 0,
@ -1391,10 +1438,7 @@
"secret": "6o8kIKQznQtejHOdRhWeKorBJclMGcgA",
"redirectUris": [
"https://api.unused-for-local-dev.spiffworkflow.org/*",
"http://localhost:7001/*",
"http://67.205.133.116:7000/*",
"http://167.172.242.138:7001/*",
"https://api.demo.spiffworkflow.org/*"
"http://localhost:8001/*"
],
"webOrigins": [],
"notBefore": 0,
@ -1712,6 +1756,20 @@
"jsonType.label": "String"
}
},
{
"id": "75c10b23-3a63-4aa6-99cc-7c8b7645d32f",
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"consentRequired": false,
"config": {
"full.path": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "groups",
"userinfo.token.claim": "true"
}
},
{
"id": "b05e679c-e00e-427e-8e47-0a4fd411c7a6",
"name": "updated at",
@ -2071,6 +2129,7 @@
"browserSecurityHeaders": {
"contentSecurityPolicyReportOnly": "",
"xContentTypeOptions": "nosniff",
"referrerPolicy": "no-referrer",
"xRobotsTag": "none",
"xFrameOptions": "SAMEORIGIN",
"contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
@ -2106,14 +2165,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
"oidc-usermodel-property-mapper"
]
}
},
@ -2135,14 +2194,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper"
"oidc-usermodel-property-mapper"
]
}
},
@ -2281,40 +2340,6 @@
}
]
},
{
"id": "7675760b-666a-4b8c-a9b8-da1e01c207fe",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "basic-auth",
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
"autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticator": "basic-auth-otp",
"authenticatorFlow": false,
"requirement": "DISABLED",
"priority": 20,
"autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticator": "auth-spnego",
"authenticatorFlow": false,
"requirement": "DISABLED",
"priority": 30,
"autheticatorFlow": false,
"userSetupAllowed": false
}
]
},
{
"id": "34e18ea8-f515-46dc-9dbf-5b79f8154564",
"alias": "Browser - Conditional OTP",
@ -2687,32 +2712,6 @@
}
]
},
{
"id": "70ef5a26-e3bb-4ba7-a05a-d205b0a3836c",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": true,
"authenticationExecutions": [
{
"authenticator": "no-cookie-redirect",
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
"autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "REQUIRED",
"priority": 20,
"autheticatorFlow": true,
"flowAlias": "Authentication Options",
"userSetupAllowed": false
}
]
},
{
"id": "89abf09a-bfb4-4dea-b164-ca7c563b4009",
"alias": "registration",
@ -2862,9 +2861,9 @@
"config": {}
},
{
"alias": "terms_and_conditions",
"alias": "TERMS_AND_CONDITIONS",
"name": "Terms and Conditions",
"providerId": "terms_and_conditions",
"providerId": "TERMS_AND_CONDITIONS",
"enabled": false,
"defaultAction": false,
"priority": 20,
@ -2940,7 +2939,7 @@
"parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0"
},
"keycloakVersion": "20.0.1",
"keycloakVersion": "22.0.4",
"userManagedAccessAllowed": false,
"clientProfiles": {
"profiles": []

View File

@ -241,12 +241,13 @@ def setup_config(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS") is None:
app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"] = [
{
"identifier": "default",
"label": "Default",
"uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"),
"additional_valid_client_ids": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_ADDITIONAL_VALID_CLIENT_IDS"),
"client_id": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID"),
"client_secret": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY"),
"additional_valid_client_ids": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_ADDITIONAL_VALID_CLIENT_IDS"),
"identifier": "default",
"internal_uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_INTERNAL_URL"),
"label": "Default",
"uri": app.config.get("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL"),
}
]

View File

@ -134,6 +134,7 @@ else:
SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL = url_config
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID", default="spiffworkflow-backend")
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q")
config_from_env("SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_INTERNAL_URL")
# comma-separated list of client ids that can be successfully validated against.
# useful for api users that will login to a different client on the same realm but from something external to backend.
@ -148,6 +149,7 @@ else:
{
"identifier": "default",
"label": "Default",
"internal_uri": "http://localhost:7002/realms/spiffworkflow-local",
"uri": "http://localhost:7002/realms/spiffworkflow-local",
"client_id": "spiffworkflow-backend",
"client_secret": "JXeQExm0JhQPLumgHtIIqf52bDalHz0q",

View File

@ -72,6 +72,7 @@ class AuthenticationProviderTypes(enum.Enum):
class AuthenticationOptionForApi(TypedDict):
identifier: str
label: str
internal_uri: NotRequired[str]
uri: str
additional_valid_client_ids: NotRequired[str]
@ -122,9 +123,14 @@ class AuthenticationService:
return [cls.client_id(authentication_identifier), "account"]
@classmethod
def server_url(cls, authentication_identifier: str) -> str:
def server_url(cls, authentication_identifier: str, internal: bool = False) -> str:
"""Returns the server url from the config."""
config: str = cls.authentication_option_for_identifier(authentication_identifier)["uri"]
auth_config = cls.authentication_option_for_identifier(authentication_identifier)
uri_key = "uri"
if internal:
if "internal_uri" in auth_config and auth_config["internal_uri"] is not None:
uri_key = "internal_uri"
config: str = auth_config[uri_key] # type: ignore
return config
@classmethod
@ -134,15 +140,16 @@ class AuthenticationService:
return config
@classmethod
def open_id_endpoint_for_name(cls, name: str, authentication_identifier: str) -> str:
def open_id_endpoint_for_name(cls, name: str, authentication_identifier: str, internal: bool = False) -> str:
"""All openid systems provide a mapping of static names to the full path of that endpoint."""
appropriate_server_url = cls.server_url(authentication_identifier)
openid_config_url = f"{appropriate_server_url}/.well-known/openid-configuration"
if authentication_identifier not in cls.ENDPOINT_CACHE:
cls.ENDPOINT_CACHE[authentication_identifier] = {}
if authentication_identifier not in cls.JSON_WEB_KEYSET_CACHE:
cls.JSON_WEB_KEYSET_CACHE[authentication_identifier] = {}
internal_server_url = cls.server_url(authentication_identifier, internal=True)
openid_config_url = f"{internal_server_url}/.well-known/openid-configuration"
if name not in AuthenticationService.ENDPOINT_CACHE[authentication_identifier]:
try:
response = safe_requests.get(openid_config_url, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
@ -151,7 +158,14 @@ class AuthenticationService:
raise OpenIdConnectionError(f"Cannot connect to given open id url: {openid_config_url}") from ce
if name not in AuthenticationService.ENDPOINT_CACHE[authentication_identifier]:
raise Exception(f"Unknown OpenID Endpoint: {name}. Tried to get from {openid_config_url}")
config: str = AuthenticationService.ENDPOINT_CACHE[authentication_identifier].get(name, "")
external_server_url = cls.server_url(authentication_identifier)
if internal is False:
if internal_server_url != external_server_url:
config = config.replace(internal_server_url, external_server_url)
return config
@classmethod
@ -166,7 +180,7 @@ class AuthenticationService:
@classmethod
def jwks_public_key_for_key_id(cls, authentication_identifier: str, key_id: str) -> JWKSKeyConfig:
jwks_uri = cls.open_id_endpoint_for_name("jwks_uri", authentication_identifier)
jwks_uri = cls.open_id_endpoint_for_name("jwks_uri", authentication_identifier, internal=True)
jwks_configs = cls.get_jwks_config_from_uri(jwks_uri)
json_key_configs: JWKSKeyConfig | None = cls.get_key_config(jwks_configs, key_id)
if not json_key_configs:
@ -303,7 +317,9 @@ class AuthenticationService:
"redirect_uri": f"{self.get_backend_url()}{redirect_url}",
}
request_url = self.open_id_endpoint_for_name("token_endpoint", authentication_identifier=authentication_identifier)
request_url = self.open_id_endpoint_for_name(
"token_endpoint", authentication_identifier=authentication_identifier, internal=True
)
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
auth_token_object: dict = json.loads(response.text)
@ -432,7 +448,9 @@ class AuthenticationService:
"client_secret": cls.secret_key(authentication_identifier),
}
request_url = cls.open_id_endpoint_for_name("token_endpoint", authentication_identifier=authentication_identifier)
request_url = cls.open_id_endpoint_for_name(
"token_endpoint", authentication_identifier=authentication_identifier, internal=True
)
response = requests.post(request_url, data=data, headers=headers, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
auth_token_object: dict = json.loads(response.text)