diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 842eef6a..c5336cc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ env: IMAGE_NAME: maykinmedia/open-archiefbeheer DJANGO_SETTINGS_MODULE: openarchiefbeheer.conf.ci DOCKER_BUILDKIT: '1' + KEYCLOAK_BASE_URL: http://localhost:28080 jobs: frontend-build: @@ -191,6 +192,24 @@ jobs: npm-ci-flags: '--legacy-peer-deps' working-directory: backend nvmrc-custom-dir: backend + + - name: Start CI docker services + run: | + docker compose up -d + working-directory: backend/docker-services/keycloak + + - name: Wait for Keycloak to be up + run: | + endpoint="${KEYCLOAK_BASE_URL}/realms/openarchiefbeheer-dev/" + realm="" + + until [ $realm ]; do + echo "Checking if Keycloak at ${KEYCLOAK_BASE_URL} is up..." + realm=$(curl "$endpoint" -s | jq -r ".realm") + sleep 2 + done + + echo "Running Keycloak with realm $realm" # See https://playwright.dev/python/docs/ci#caching-browsers - name: Cache Playwright browser diff --git a/backend/docker-services/keycloak/Readme.md b/backend/docker-services/keycloak/Readme.md new file mode 100644 index 00000000..274e1915 --- /dev/null +++ b/backend/docker-services/keycloak/Readme.md @@ -0,0 +1,5 @@ +To export the realm settings, run this command in the keycloak container: + +```bash +/opt/keycloak/bin/kc.sh export --file /tmp/realm_export.json --realm openarchiefbeheer-dev +``` \ No newline at end of file diff --git a/backend/docker-services/keycloak/docker-compose.yaml b/backend/docker-services/keycloak/docker-compose.yaml new file mode 100644 index 00000000..997ff4c7 --- /dev/null +++ b/backend/docker-services/keycloak/docker-compose.yaml @@ -0,0 +1,42 @@ +# +# DISCLAIMER: THIS IS FOR DEVELOPMENT PURPOSES ONLY AND NOT SUITABLE FOR PRODUCTION. +# +# You can use this docker-compose to spin up a local stack for demo/try-out +# purposes, or to get some insight in the various components involved (e.g. to build +# your Helm charts from). Note that various environment variables are UNSAFE and merely +# specified so that you can get up and running with the least amount of friction. +services: + keycloak-db: + image: postgres:14 + restart: unless-stopped + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: keycloak + networks: + - keycloak-dev + + keycloak: + depends_on: + - keycloak-db + container_name: keycloak_dev + command: start-dev --import-realm + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://keycloak-db/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KC_HOSTNAME: localhost + image: quay.io/keycloak/keycloak:25.0.6 + ports: + - "28080:8080" + restart: unless-stopped + volumes: + - ./fixtures:/opt/keycloak/data/import + networks: + - keycloak-dev + +networks: + keycloak-dev: \ No newline at end of file diff --git a/backend/docker-services/keycloak/fixtures/realm_export.json b/backend/docker-services/keycloak/fixtures/realm_export.json new file mode 100644 index 00000000..8551a7f8 --- /dev/null +++ b/backend/docker-services/keycloak/fixtures/realm_export.json @@ -0,0 +1,1975 @@ +{ + "id" : "openarchiefbeheer-dev", + "realm" : "openarchiefbeheer-dev", + "displayName" : "Open Archiefbeheer Dev", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "66a761ef-9dfe-4b52-817c-66b4ec1c3314", + "name" : "default-roles-openarchiefbeheer-dev", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "openarchiefbeheer-dev", + "attributes" : { } + }, { + "id" : "8be40cf1-253b-41bd-947c-f2327a7b9b09", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "openarchiefbeheer-dev", + "attributes" : { } + }, { + "id" : "58cd0d34-1450-4206-8db1-d04492eebeba", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "openarchiefbeheer-dev", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "da52d197-34f1-4644-8cb9-bdc1e83ef5e9", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "8c54f5e7-0380-4bf7-a9d1-e32194ef7808", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "c21429a3-d96b-4b25-b557-9315a5320674", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "bb5ecd2a-4cdd-402e-b5e6-c3b8dae68bf6", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "4f15647e-d14b-4e62-a3b5-e4c1f94a5855", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "88f397e9-c54b-47b0-a22d-1af2e5fa3cd3", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "07918027-0387-4709-8448-a5a1e2437a7d", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "36a62e00-0710-4fd0-a470-a2dce508a8f7", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "486d8e84-c5ed-4f79-9ad1-2ea49c077606", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "5fbfdb68-7c07-4dbe-8c1a-fb06f213cc5c", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "50e5027b-8817-4ba5-96d0-403e8bd3a70e", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "95121c14-45ba-4b14-a737-af28342b9902", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "32dcaf0c-3504-48d6-a795-fa5786db453d", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "0247d8c4-ac4a-492f-ac89-72cf2e645cf8", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "beb29429-d3ca-4d58-a9d9-5d0fcbac346c", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "59657295-c26e-4332-abdb-754a071b20bd", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "95421ee7-a6d8-404c-b0a9-bd861114a7e5", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "bbe1ca2d-dd0e-4d02-8e24-0106d8e778cc", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + }, { + "id" : "475ecd33-7fba-4549-a9c4-119cba466b0b", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "manage-clients", "manage-identity-providers", "view-realm", "view-users", "manage-events", "view-events", "view-identity-providers", "query-realms", "query-clients", "manage-authorization", "view-authorization", "query-groups", "create-client", "manage-realm", "impersonation", "view-clients", "manage-users", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "21c99f9f-7147-4fb6-b7d3-17226ee55fae", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "5a2dae32-2af9-4ad3-9896-fba67d6909e3", + "attributes" : { } + } ], + "openarchiefbeheer-dev" : [ { + "id" : "81c6c73e-c2ca-4503-960b-2504ea9d77e4", + "name" : "Record Manager", + "composite" : false, + "clientRole" : true, + "containerId" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "attributes" : { } + }, { + "id" : "a0974f2f-7143-497d-a380-c9da8ac84871", + "name" : "Administrator", + "composite" : false, + "clientRole" : true, + "containerId" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "attributes" : { } + }, { + "id" : "e71a7e56-135a-48b5-bb1e-2a5c01603694", + "name" : "Superuser", + "composite" : false, + "clientRole" : true, + "containerId" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "attributes" : { } + }, { + "id" : "baa57413-0c52-4690-bc0f-d6664746894e", + "name" : "Reviewer", + "composite" : false, + "clientRole" : true, + "containerId" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "attributes" : { } + }, { + "id" : "5a39397a-b057-4fa5-a275-d55f01b88f79", + "name" : "Archivist", + "composite" : false, + "clientRole" : true, + "containerId" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "attributes" : { } + } ], + "account" : [ { + "id" : "1e824f80-c472-4984-85d2-3e5f167bc926", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "7b9bedc9-b476-49a3-841e-9ccfc4bf1073", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "019d375c-0dfa-431c-904c-52685c7a5fad", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "2b0d6417-7aba-440a-a2fb-73e37619d9cf", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "2528402d-cb98-4f0d-aae3-604f78977425", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "65b4885d-7c5e-4f43-bca4-806b9ea15dc4", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "d258dc23-633d-4f08-8f8d-c3630edb96d8", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + }, { + "id" : "7de41950-fd87-4c7f-a1ae-d13082df7fe7", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "attributes" : { } + } ] + } + }, + "groups" : [ { + "id" : "2c085292-8b23-41b7-991b-b62ad4832f5a", + "name" : "Admins", + "path" : "/Admins", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Superuser" ] + } + }, { + "id" : "0ba01c88-dd04-42a7-93de-7025c8c12fde", + "name" : "OAB - Administrator", + "path" : "/OAB - Administrator", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Administrator" ] + } + }, { + "id" : "d5c6265d-25f4-4c8b-804b-47b0928686a9", + "name" : "OAB - Archivist", + "path" : "/OAB - Archivist", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Archivist" ] + } + }, { + "id" : "4a753bc4-fd20-4f79-a7d4-d834e18a23eb", + "name" : "OAB - Record Manager", + "path" : "/OAB - Record Manager", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Record Manager" ] + } + }, { + "id" : "2ceacd02-0669-47b5-b05c-96cda6af85ae", + "name" : "OAB - Reviewer", + "path" : "/OAB - Reviewer", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Reviewer" ] + } + }, { + "id" : "8f4db3be-87cd-4e1b-97fe-c2dc7a48f2de", + "name" : "Test Admins", + "path" : "/Test Admins", + "subGroups" : [ ], + "attributes" : { }, + "realmRoles" : [ ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Superuser" ] + } + } ], + "defaultRole" : { + "id" : "66a761ef-9dfe-4b52-817c-66b4ec1c3314", + "name" : "default-roles-openarchiefbeheer-dev", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "openarchiefbeheer-dev" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "users" : [ { + "id" : "f4a4a679-d2fb-41aa-a886-ffff824335b7", + "username" : "alice_doe", + "firstName" : "Alice", + "lastName" : "Doe", + "email" : "alice_doe@oab.test", + "emailVerified" : true, + "createdTimestamp" : 1728047925356, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "8c61d30e-c169-4614-8c40-016528ec8d70", + "type" : "password", + "createdDate" : 1728047925376, + "secretData" : "{\"value\":\"opK2JwNdEiwrKJgKzYdalxOTSxuX0Kh+0nsLMCZNDTQ=\",\"salt\":\"cFXf3ITG9X9kJDuOlYw2Xw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-openarchiefbeheer-dev" ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Record Manager" ] + }, + "notBefore" : 0, + "groups" : [ "/OAB - Record Manager" ] + }, { + "id" : "5aca5d15-ab33-4f05-a335-4ac764869121", + "username" : "john_doe", + "firstName" : "John", + "lastName" : "Doe", + "email" : "john_doe@oab.test", + "emailVerified" : true, + "createdTimestamp" : 1728047830667, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "963a8b59-46f6-4727-a840-7e75e5e70429", + "type" : "password", + "createdDate" : 1728047830684, + "secretData" : "{\"value\":\"Rk+YGN/R1wUDE+EUW6hDoP4EmKngH3Pt+6fnolmIpBc=\",\"salt\":\"CHhMQb3A/xk8tAHjNDVFxg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-openarchiefbeheer-dev" ], + "clientRoles" : { + "openarchiefbeheer-dev" : [ "Superuser" ] + }, + "notBefore" : 0, + "groups" : [ "/Admins" ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "4be4b0c5-bc13-4293-8433-0de32656f994", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/openarchiefbeheer-dev/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/openarchiefbeheer-dev/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "8ef72c09-d7f3-4c60-b889-069893b13a76", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/openarchiefbeheer-dev/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/openarchiefbeheer-dev/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "aeaedcc7-1f80-4fd2-89a1-69ceb79c2083", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b47bbfa0-0bb3-4609-ba1f-b87e4f93d218", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "5a2dae32-2af9-4ad3-9896-fba67d6909e3", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e727c82d-99d0-4e9f-8f37-a77821a1dfc6", + "clientId" : "openarchiefbeheer-dev", + "name" : "openarchiefbeheer-dev", + "description" : "openarchiefbeheer-dev", + "rootUrl" : "http://localhost:8000", + "adminUrl" : "http://localhost:8000/admin", + "baseUrl" : "/admin", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "http://localhost:8000/oidc/callback/", "http://localhost:3000" ], + "webOrigins" : [ "http://localhost:8000", "http://localhost:3000" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1728044648", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "display.on.consent.screen" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "1dc61b83-d532-4274-a891-c6303607f7d8", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e40e4494-f018-4855-873b-4d0ca510fdd3", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/openarchiefbeheer-dev/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/openarchiefbeheer-dev/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "336a0717-75be-47a9-8468-631f4a05e6c3", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "2374f413-6347-4c87-a30a-e673191a12a7", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "956a094d-3902-40de-8ad8-9a44ab9bd7d0", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "864376f1-e69d-4058-9dbc-ec207f68e40a", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "9edb2415-cbc1-45a7-8064-7fc95d0293a0", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "dd529412-e8d8-4a19-948e-f1c054ce88f1", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "${rolesScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "fc0ccdbb-9e1d-459a-8f1a-7d0520d5eed5", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "access.token.claim" : "true", + "introspection.token.claim" : "true" + } + }, { + "id" : "7a622cff-0ce3-499b-8234-6f4ba1a0cb26", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "false", + "lightweight.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String" + } + }, { + "id" : "2803dae0-1588-4abe-9210-50e82abdd739", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "57354af2-c2ef-4f9a-a6bd-31c5576cde76", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${profileScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "287f9126-9c30-40cb-9fe6-1735a4674706", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "f7e66117-b9f2-4c27-9462-2c002f43a18a", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "0dcf6a40-3517-4a27-a94c-e85bf716a058", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "4d3bba50-da7a-45b0-96e0-5286cb3ffe56", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "22a7c46e-a70f-40e8-b320-3e02081da588", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "ba906985-5f31-415c-9c29-f4eedc4cdc74", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "b4cd3d11-16f6-4f50-a275-efe5773c9b78", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "aa863daf-296a-4e2e-a233-cf1f4310a395", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "dbe0aeaa-7739-47d8-8cb3-782534273d45", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "eb248a11-659e-4eec-b8f6-4d4409d3940f", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "43ac784f-a78a-4ce3-bc7f-2ac484f92569", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "2797d2f8-5d8c-4116-9a71-3826786b79a3", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "dee259bd-df39-418b-8f67-7719e11152cf", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "28a08852-0c7d-400e-8b67-5d2a200daf03", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "f2986101-515b-45ea-9ef9-7ae9a268eb80", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${emailScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "4d9a848c-4ab7-49e4-98af-0075943b87dd", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "3c641400-7284-4899-8433-adb8919b986f", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "47450af5-9748-48a1-a565-06447d53bf22", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${addressScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "6adebf5a-1ba0-4846-9629-444de7890bc4", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "10d00ff4-2060-4bd4-8c89-88a3b8804cdf", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "efb6f28e-1a5d-48bc-8af6-79e8175863a9", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "b670e70d-2451-419c-b2de-00e40b76a919", + "name" : "basic", + "description" : "OpenID Connect scope for add all basic claims to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "cec610b9-701a-4fdb-87bb-04b515c73618", + "name" : "sub", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-sub-mapper", + "consentRequired" : false, + "config" : { + "access.token.claim" : "true", + "introspection.token.claim" : "true" + } + }, { + "id" : "d5383ade-1d6d-4101-a9ca-6c01288020d8", + "name" : "auth_time", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "AUTH_TIME", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "auth_time", + "jsonType.label" : "long" + } + } ] + }, { + "id" : "bd3a0025-5c1b-40da-944b-0c4eb34f7056", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "14b04512-29df-4fdd-924d-cc2cf422fa76", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "access.token.claim" : "true", + "introspection.token.claim" : "true" + } + } ] + }, { + "id" : "3d16adcd-9f49-4a3b-b559-779f6780cb67", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${phoneScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "66ace8a9-9ae4-4941-b07e-c5f43c6e3005", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "c54b157e-9e81-4c4c-a073-901576ffdded", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "65229d55-19b6-4acb-a2b4-37e816f3ca11", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "c2da6799-10dd-40f2-b7fb-c1f583dd819b", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "basic" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "31d1416b-9e40-4b44-b498-a8629389324e", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "d30be275-0158-4e21-bfc1-e8194712c53b", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "6b3f4664-8924-46e1-b9ab-ba77184d5983", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "1b2b0f4b-3c58-464a-8ab3-3b71295b60ed", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "37da4b28-8e1b-4f8b-aed9-d16f36df636b", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "863f9396-1d28-4cc4-bd48-072ccd43e0c6", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "6577aea9-8360-42fb-a44c-6f7bf637a1dc", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "ba43a51d-1298-4bff-be36-15d15a77a552", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "95172407-926b-4f55-b19a-ce962df63129", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "09305558-484d-435e-8914-92a0dffb8444", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "7ccffcf5-dafa-47aa-8e6a-fccd0524104a", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "25570873-b366-4a81-b0de-68699e94da6d", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "1b57dc2e-7b6d-4add-913e-c1b577fc07d2", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "88272dc9-c7cd-4926-8f72-edf848e9960a", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e514b8f6-1f63-4a80-bc44-e6837441f7f7", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "4890d04b-92c6-43a4-9e93-00612fff9273", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e9313ad6-e4b7-4ae0-93e6-e7c8d9632e3b", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "6e31f568-e94e-425a-ae36-0f4a43762ad2", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "72da64a1-338f-4490-b425-ed1df8a7cee2", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "175f4eb0-d332-461c-aca4-4a025faedf2c", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "3dec7c19-8827-4afd-8de9-a8fcfbafc27a", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "d2020bf3-8030-4a63-b936-4941a7b61453", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "32ad19bc-5737-4e95-b42e-f18cf44b15e7", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "37bf4d2f-2e8b-41d1-8d6a-114bbbd5b12d", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-terms-and-conditions", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 70, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e1ead4b0-88ab-4cdb-83bd-9142bcf92a68", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "0faa0415-c97b-4a37-a98b-b9c42de83f64", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "58314914-e347-483a-8e61-79da1d55c4c5", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "96e0847c-9ce5-4803-82c3-49ca0cc3e5fa", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "VERIFY_PROFILE", + "name" : "Verify Profile", + "providerId" : "VERIFY_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 90, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaAuthRequestedUserHint" : "login_hint", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false", + "cibaExpiresIn" : "120", + "oauth2DeviceCodeLifespan" : "600", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "organizationsEnabled" : "false" + }, + "keycloakVersion" : "25.0.6", + "userManagedAccessAllowed" : false, + "organizationsEnabled" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file diff --git a/backend/docker-services/openzaak/docker-compose.yaml b/backend/docker-services/openzaak/docker-compose.yaml index 68d0b40f..46948e54 100644 --- a/backend/docker-services/openzaak/docker-compose.yaml +++ b/backend/docker-services/openzaak/docker-compose.yaml @@ -1,5 +1,10 @@ -version: '3.8' - +# +# DISCLAIMER: THIS IS FOR DEVELOPMENT PURPOSES ONLY AND NOT SUITABLE FOR PRODUCTION. +# +# You can use this docker-compose to spin up a local stack for demo/try-out +# purposes, or to get some insight in the various components involved (e.g. to build +# your Helm charts from). Note that various environment variables are UNSAFE and merely +# specified so that you can get up and running with the least amount of friction. name: open-zaak services: diff --git a/backend/docs/developers/e2e-tests.rst b/backend/docs/developers/e2e-tests.rst index da3a91cf..dbcca85a 100644 --- a/backend/docs/developers/e2e-tests.rst +++ b/backend/docs/developers/e2e-tests.rst @@ -75,3 +75,17 @@ To do this, replace the context manager ``browser_page`` in the test that is fai In the ``.github/workflows/ci.yaml``, go to the e2e-tests and uncomment the last step. This will upload the recorded trace so that you can download it and look at it. +Keycloak tests +============== + +To run the E2E tests that check the OIDC login, Keycloak needs to be running locally. + +There is a ``docker-compose.yaml`` file to run Keycloak locally. It is located in the ``backend/docker-services/keycloak`` folder. +Inside this folder, there is also a fixture which loads an ``openarchiefbeheer-dev`` realm, with the roles/groups already configured. +The fixture is automatically loaded into Keycloak when the container is started. +There are also two users: + + * John Doe (``john_doe`` / ``aNic3Passw0rd``) who is a superuser. + * Alice Doe (``alice_doe`` / ``aNic3Passw0rd``) who is a record manager. + +There is also a Keycloak admin user (``admin`` / ``admin``) that can be used to log into the Keycloak admin. \ No newline at end of file diff --git a/backend/dotenv.dev.example b/backend/dotenv.dev.example index 4d7e4bf5..d712c39c 100644 --- a/backend/dotenv.dev.example +++ b/backend/dotenv.dev.example @@ -3,12 +3,15 @@ DISABLE_2FA=yes ALLOWED_HOSTS=localhost CORS_ALLOWED_ORIGINS=http://localhost:8000,http://localhost:3000 CSRF_TRUSTED_ORIGINS=http://localhost:3000 -CSRF_COOKIE_SAMESITE='None' +CSRF_COOKIE_SAMESITE='Lax' CSRF_COOKIE_SECURE=False -SESSION_COOKIE_SAMESITE='None' +SESSION_COOKIE_SAMESITE='Lax' SESSION_COOKIE_SECURE=False +# For OIDC +OIDC_REDIRECT_ALLOWED_HOSTS=localhost:3000 + # For openzaak being slow REQUESTS_READ_TIMEOUT=5000 diff --git a/backend/requirements/base.in b/backend/requirements/base.in index 5a3cb450..005e6b0c 100644 --- a/backend/requirements/base.in +++ b/backend/requirements/base.in @@ -14,6 +14,7 @@ django-rosetta maykin-2fa django-timeline-logger django-solo +mozilla-django-oidc-db # API libraries djangorestframework diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index eb12d3f4..86943cf2 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -12,10 +12,15 @@ asn1crypto==1.5.1 # via webauthn attrs==23.2.0 # via + # glom # jsonschema # referencing billiard==4.2.0 # via celery +boltons==24.0.0 + # via + # face + # glom cbor2==5.6.3 # via webauthn celery==5.4.0 @@ -44,6 +49,8 @@ click-repl==0.3.0 cryptography==43.0.1 # via # django-simple-certmanager + # josepy + # mozilla-django-oidc # pyopenssl # webauthn django==4.2.16 @@ -56,6 +63,7 @@ django==4.2.16 # django-filter # django-formtools # django-hijack + # django-jsonform # django-otp # django-phonenumber-field # django-privates @@ -70,6 +78,8 @@ django==4.2.16 # djangorestframework # drf-spectacular # maykin-2fa + # mozilla-django-oidc + # mozilla-django-oidc-db # zgw-consumers django-admin-index==3.1.1 # via -r requirements/base.in @@ -85,6 +95,8 @@ django-formtools==2.5.1 # via django-two-factor-auth django-hijack==3.4.5 # via -r requirements/base.in +django-jsonform==2.23.0 + # via mozilla-django-oidc-db django-ordered-model==3.7.4 # via django-admin-index django-otp==1.5.0 @@ -106,6 +118,7 @@ django-simple-certmanager==2.0.0 django-solo==2.2.0 # via # -r requirements/base.in + # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==4.0.0 # via -r requirements/base.in @@ -126,14 +139,20 @@ ecs-logging==2.1.0 # via elastic-apm elastic-apm==6.22.0 # via -r requirements/base.in +face==20.1.1 + # via glom furl==2.1.3 # via # -r requirements/base.in # ape-pie +glom==23.5.0 + # via mozilla-django-oidc-db idna==3.7 # via requests inflection==0.5.1 # via drf-spectacular +josepy==1.14.0 + # via mozilla-django-oidc jsonschema==4.21.1 # via drf-spectacular jsonschema-specifications==2023.12.1 @@ -142,6 +161,10 @@ kombu==5.3.7 # via celery maykin-2fa==1.0.0 # via -r requirements/base.in +mozilla-django-oidc==4.0.1 + # via mozilla-django-oidc-db +mozilla-django-oidc-db==0.19.0 + # via -r requirements/base.in orderedmultidict==1.0.1 # via furl phonenumberslite==8.13.35 @@ -159,7 +182,9 @@ pycparser==2.22 pyjwt==2.8.0 # via zgw-consumers pyopenssl==24.2.1 - # via webauthn + # via + # josepy + # webauthn pypng==0.20220715.0 # via qrcode python-dateutil==2.9.0.post0 @@ -184,6 +209,7 @@ requests==2.32.3 # via # ape-pie # django-rosetta + # mozilla-django-oidc # zgw-consumers rpds-py==0.18.0 # via @@ -200,6 +226,7 @@ sqlparse==0.5.0 # via django typing-extensions==4.11.0 # via + # mozilla-django-oidc-db # qrcode # zgw-consumers tzdata==2024.1 diff --git a/backend/requirements/ci.txt b/backend/requirements/ci.txt index 4bd50a70..af399144 100644 --- a/backend/requirements/ci.txt +++ b/backend/requirements/ci.txt @@ -29,6 +29,7 @@ attrs==23.2.0 # via # -c requirements/base.txt # -r requirements/base.txt + # glom # jsonschema # referencing babel==2.14.0 @@ -42,6 +43,12 @@ billiard==4.2.0 # celery black==24.4.2 # via -r requirements/test-tools.in +boltons==24.0.0 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # face + # glom cbor2==5.6.3 # via # -c requirements/base.txt @@ -99,6 +106,8 @@ cryptography==43.0.1 # -c requirements/base.txt # -r requirements/base.txt # django-simple-certmanager + # josepy + # mozilla-django-oidc # pyopenssl # webauthn cssselect==1.2.0 @@ -117,6 +126,7 @@ django==4.2.16 # django-formtools # django-hijack # django-jenkins + # django-jsonform # django-otp # django-phonenumber-field # django-privates @@ -131,6 +141,8 @@ django==4.2.16 # djangorestframework # drf-spectacular # maykin-2fa + # mozilla-django-oidc + # mozilla-django-oidc-db # zgw-consumers django-admin-index==3.1.1 # via @@ -164,6 +176,11 @@ django-hijack==3.4.5 # -r requirements/base.txt django-jenkins==0.110.0 # via -r requirements/test-tools.in +django-jsonform==2.23.0 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # mozilla-django-oidc-db django-ordered-model==3.7.4 # via # -c requirements/base.txt @@ -211,6 +228,7 @@ django-solo==2.2.0 # via # -c requirements/base.txt # -r requirements/base.txt + # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==4.0.0 # via @@ -256,6 +274,11 @@ elastic-apm==6.22.0 # via # -c requirements/base.txt # -r requirements/base.txt +face==20.1.1 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # glom factory-boy==3.3.0 # via -r requirements/test-tools.in faker==24.14.1 @@ -269,6 +292,11 @@ furl==2.1.3 # -c requirements/base.txt # -r requirements/base.txt # ape-pie +glom==23.5.0 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # mozilla-django-oidc-db greenlet==3.0.3 # via playwright idna==3.7 @@ -290,6 +318,11 @@ isort==5.13.2 # via pylint jinja2==3.1.4 # via sphinx +josepy==1.14.0 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # mozilla-django-oidc jsonschema==4.21.1 # via # -c requirements/base.txt @@ -317,6 +350,15 @@ mccabe==0.7.0 # via # flake8 # pylint +mozilla-django-oidc==4.0.1 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # mozilla-django-oidc-db +mozilla-django-oidc-db==0.19.0 + # via + # -c requirements/base.txt + # -r requirements/base.txt multidict==6.0.5 # via yarl mypy-extensions==1.0.0 @@ -388,6 +430,7 @@ pyopenssl==24.2.1 # via # -c requirements/base.txt # -r requirements/base.txt + # josepy # webauthn pypng==0.20220715.0 # via @@ -452,6 +495,7 @@ requests==2.32.3 # ape-pie # django-rosetta # docker + # mozilla-django-oidc # pytest-base-url # requests-mock # sphinx @@ -517,6 +561,7 @@ typing-extensions==4.11.0 # via # -c requirements/base.txt # -r requirements/base.txt + # mozilla-django-oidc-db # pyee # qrcode # zgw-consumers diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index 6d84637a..df0c99cd 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -35,6 +35,7 @@ attrs==23.2.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # glom # jsonschema # referencing babel==2.14.0 @@ -57,6 +58,12 @@ black==24.4.2 # -c requirements/ci.txt # -r requirements/ci.txt # -r requirements/dev.in +boltons==24.0.0 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # face + # glom build==1.2.1 # via pip-tools bump2version==1.0.1 @@ -123,6 +130,8 @@ cryptography==43.0.1 # -c requirements/ci.txt # -r requirements/ci.txt # django-simple-certmanager + # josepy + # mozilla-django-oidc # pyopenssl # webauthn cssselect==1.2.0 @@ -149,6 +158,7 @@ django==4.2.16 # django-formtools # django-hijack # django-jenkins + # django-jsonform # django-otp # django-phonenumber-field # django-privates @@ -163,6 +173,8 @@ django==4.2.16 # djangorestframework # drf-spectacular # maykin-2fa + # mozilla-django-oidc + # mozilla-django-oidc-db # zgw-consumers django-admin-index==3.1.1 # via @@ -202,6 +214,11 @@ django-jenkins==0.110.0 # via # -c requirements/ci.txt # -r requirements/ci.txt +django-jsonform==2.23.0 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # mozilla-django-oidc-db django-ordered-model==3.7.4 # via # -c requirements/ci.txt @@ -249,6 +266,7 @@ django-solo==2.2.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # mozilla-django-oidc-db # zgw-consumers django-timeline-logger==4.0.0 # via @@ -300,6 +318,11 @@ elastic-apm==6.22.0 # via # -c requirements/ci.txt # -r requirements/ci.txt +face==20.1.1 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # glom factory-boy==3.3.0 # via # -c requirements/ci.txt @@ -326,6 +349,11 @@ gitdb==4.0.11 # via gitpython gitpython==3.1.43 # via -r requirements/dev.in +glom==23.5.0 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # mozilla-django-oidc-db greenlet==3.0.3 # via # -c requirements/ci.txt @@ -363,6 +391,11 @@ jinja2==3.1.4 # -c requirements/ci.txt # -r requirements/ci.txt # sphinx +josepy==1.14.0 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # mozilla-django-oidc jsonschema==4.21.1 # via # -c requirements/ci.txt @@ -398,6 +431,15 @@ mccabe==0.7.0 # -r requirements/ci.txt # flake8 # pylint +mozilla-django-oidc==4.0.1 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # mozilla-django-oidc-db +mozilla-django-oidc-db==0.19.0 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt multidict==6.0.5 # via # -c requirements/ci.txt @@ -507,6 +549,7 @@ pyopenssl==24.2.1 # via # -c requirements/ci.txt # -r requirements/ci.txt + # josepy # webauthn pypng==0.20220715.0 # via @@ -586,6 +629,7 @@ requests==2.32.3 # ape-pie # django-rosetta # docker + # mozilla-django-oidc # pytest-base-url # requests-mock # sphinx @@ -700,6 +744,7 @@ typing-extensions==4.11.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # mozilla-django-oidc-db # pyee # qrcode # zgw-consumers diff --git a/backend/src/openarchiefbeheer/accounts/migrations/0007_add_superuser_group.py b/backend/src/openarchiefbeheer/accounts/migrations/0007_add_superuser_group.py new file mode 100644 index 00000000..5095d0fb --- /dev/null +++ b/backend/src/openarchiefbeheer/accounts/migrations/0007_add_superuser_group.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-10-04 14:35 + +from django.db import migrations + + +def create_superuser_group(apps, schema_editor): + Group = apps.get_model("auth", "Group") + + Group.objects.get_or_create(name="Superuser") + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0006_remove_user_role_delete_role"), + ] + + operations = [ + migrations.RunPython(create_superuser_group, migrations.RunPython.noop), + ] diff --git a/backend/src/openarchiefbeheer/api/tests/test_oidc_auth.py b/backend/src/openarchiefbeheer/api/tests/test_oidc_auth.py new file mode 100644 index 00000000..773fd075 --- /dev/null +++ b/backend/src/openarchiefbeheer/api/tests/test_oidc_auth.py @@ -0,0 +1,90 @@ +from django.test import tag + +from playwright.async_api import expect + +from openarchiefbeheer.utils.tests.e2e import browser_page +from openarchiefbeheer.utils.tests.gherkin import GherkinLikeTestCase + + +@tag("e2e") +class OIDCLoginTest(GherkinLikeTestCase): + fixtures = ["permissions.json", "oidc_config_test.json"] + + async def test_login_admin_superuser_with_oidc(self): + async with browser_page() as page: + await page.goto(f"{self.live_server_url}/admin") + + link = page.get_by_role("link", name="Login with OIDC") + + await expect(link).to_be_visible() + + await link.click() + + username_field = page.get_by_role("textbox", name="username") + await username_field.fill( + "john_doe" + ) # configured in the Keycloak fixture as superuser + + password_field = page.get_by_role("textbox", name="password") + await password_field.fill("aNic3Passw0rd") + + login_button = page.get_by_role("button", name="Sign In") + await login_button.click() + + await page.wait_for_url(f"{self.live_server_url}/admin/") + + configuration_link = page.get_by_role("link", name="API configuration") + await expect(configuration_link).to_be_visible() + + async def test_login_admin_staff_with_oidc(self): + async with browser_page() as page: + await page.goto(f"{self.live_server_url}/admin") + + link = page.get_by_role("link", name="Login with OIDC") + + await expect(link).to_be_visible() + + await link.click() + + username_field = page.get_by_role("textbox", name="username") + await username_field.fill( + "alice_doe" + ) # configured in the Keycloak fixture as record manager + + password_field = page.get_by_role("textbox", name="password") + await password_field.fill("aNic3Passw0rd") + + login_button = page.get_by_role("button", name="Sign In") + await login_button.click() + + await page.wait_for_url(f"{self.live_server_url}/admin/") + + page_text = page.get_by_text("You don't have permission to") + await expect(page_text).to_be_visible() + + async def test_login_app_with_oidc(self): + async with browser_page() as page: + await page.goto(self.live_server_url) + await page.wait_for_url( + f"{self.live_server_url}/login?next=/destruction-lists" + ) + + login_button = page.get_by_text("Organisatie login") + + await login_button.click() + + username_field = page.get_by_role("textbox", name="username") + await username_field.fill( + "alice_doe" + ) # configured in the Keycloak fixture as record manager + + password_field = page.get_by_role("textbox", name="password") + await password_field.fill("aNic3Passw0rd") + + login_button = page.get_by_role("button", name="Sign In") + await login_button.click() + + await page.wait_for_url(f"{self.live_server_url}/destruction-lists") + + page_text = page.get_by_role("heading", name="Vernietigingslijsten") + await expect(page_text).to_be_visible() diff --git a/backend/src/openarchiefbeheer/api/urls.py b/backend/src/openarchiefbeheer/api/urls.py index 40c7b1fe..09ad8300 100644 --- a/backend/src/openarchiefbeheer/api/urls.py +++ b/backend/src/openarchiefbeheer/api/urls.py @@ -12,7 +12,7 @@ ReviewersView, WhoAmIView, ) -from openarchiefbeheer.config.api.views import ArchiveConfigView +from openarchiefbeheer.config.api.views import ArchiveConfigView, OIDCInfoView from openarchiefbeheer.destruction.api.views import ListStatusesListView from openarchiefbeheer.destruction.api.viewsets import ( DestructionListItemReviewViewSet, @@ -101,6 +101,7 @@ path( "archive-config", ArchiveConfigView.as_view(), name="archive-config" ), + path("oidc-info", OIDCInfoView.as_view(), name="oidc-info"), path( "_retrieve_zaken/", CacheZakenView.as_view(), name="retrieve-zaken" ), diff --git a/backend/src/openarchiefbeheer/conf/base.py b/backend/src/openarchiefbeheer/conf/base.py index 576194e5..a58437a1 100644 --- a/backend/src/openarchiefbeheer/conf/base.py +++ b/backend/src/openarchiefbeheer/conf/base.py @@ -128,6 +128,9 @@ "django_filters", "solo", "ordered_model", + "django_jsonform", + "mozilla_django_oidc", + "mozilla_django_oidc_db", # Project applications. "openarchiefbeheer.accounts", "openarchiefbeheer.destruction", @@ -151,6 +154,7 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "hijack.middleware.HijackUserMiddleware", + "mozilla_django_oidc_db.middleware.SessionRefresh", # should be last according to docs "axes.middleware.AxesMiddleware", "djangorestframework_camel_case.middleware.CamelCaseMiddleWare", @@ -322,6 +326,7 @@ "axes.backends.AxesBackend", "openarchiefbeheer.accounts.backends.UserModelEmailBackend", "django.contrib.auth.backends.ModelBackend", + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", ] SESSION_COOKIE_NAME = "openarchiefbeheer_sessionid" @@ -458,7 +463,7 @@ # add entries from AUTHENTICATION_BACKENDS that already enforce their own two-factor # auth, avoiding having some set up MFA again in the project. MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = [ - # "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", ] # @@ -622,3 +627,12 @@ "schedule": crontab(hour="12", minute="0"), }, } + +# +# Django OIDC +# +OIDC_AUTHENTICATE_CLASS = "mozilla_django_oidc_db.views.OIDCAuthenticationRequestView" +OIDC_CALLBACK_CLASS = "mozilla_django_oidc_db.views.OIDCCallbackView" +OIDC_REDIRECT_ALLOWED_HOSTS = config( + "OIDC_REDIRECT_ALLOWED_HOSTS", default="", split=True +) diff --git a/backend/src/openarchiefbeheer/config/api/serializers.py b/backend/src/openarchiefbeheer/config/api/serializers.py index abf865bd..699202c7 100644 --- a/backend/src/openarchiefbeheer/config/api/serializers.py +++ b/backend/src/openarchiefbeheer/config/api/serializers.py @@ -1,4 +1,9 @@ +from django.utils.translation import gettext_lazy as _ + +from drf_spectacular.utils import extend_schema_field +from mozilla_django_oidc_db.models import OpenIDConnectConfig from rest_framework import serializers +from rest_framework.reverse import reverse from ..models import ArchiveConfig @@ -21,3 +26,27 @@ class Meta: "resultaattype": {"required": True, "allow_null": False}, "informatieobjecttype": {"required": True, "allow_null": False}, } + + +class OIDCInfoSerializer(serializers.ModelSerializer): + login_url = serializers.SerializerMethodField( + label=_("OIDC authentication URL"), + help_text=_( + "URL where to start the OIDC login flow if it is enabled. If it is not enabled, it will be an empty string." + ), + ) + + class Meta: + model = OpenIDConnectConfig + fields = ( + "enabled", + "login_url", + ) + + @extend_schema_field(serializers.URLField) + def get_login_url(self, config: OpenIDConnectConfig) -> str: + if not config.enabled: + return "" + + request = self.context.get("request") + return reverse("oidc_authentication_init", request=request) diff --git a/backend/src/openarchiefbeheer/config/api/views.py b/backend/src/openarchiefbeheer/config/api/views.py index 18192c56..16978d9e 100644 --- a/backend/src/openarchiefbeheer/config/api/views.py +++ b/backend/src/openarchiefbeheer/config/api/views.py @@ -1,13 +1,15 @@ from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema +from mozilla_django_oidc_db.models import OpenIDConnectConfig +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView from openarchiefbeheer.destruction.api.permissions import CanStartDestructionPermission from ..models import ArchiveConfig -from .serializers import ArchiveConfigSerializer +from .serializers import ArchiveConfigSerializer, OIDCInfoSerializer class ArchiveConfigView(APIView): @@ -60,3 +62,21 @@ def put(self, request, *args, **kwargs) -> Response: ) def patch(self, request, *args, **kwargs) -> Response: return self.update(partial=True) + + +class OIDCInfoView(APIView): + authentication_classes = () + permission_classes = () + + @extend_schema( + summary=_("Retrieve OIDC info"), + description=_("Returns info about OIDC that is needed by the frontend. "), + tags=["Configuration"], + responses={ + 200: OIDCInfoSerializer, + }, + ) + def get(self, request: Request, *args, **kwargs): + config = OpenIDConnectConfig.get_solo() + serializer = OIDCInfoSerializer(instance=config, context={"request": request}) + return Response(serializer.data) diff --git a/backend/src/openarchiefbeheer/config/tests/test_views.py b/backend/src/openarchiefbeheer/config/tests/test_views.py index 6c1132ce..e7082d14 100644 --- a/backend/src/openarchiefbeheer/config/tests/test_views.py +++ b/backend/src/openarchiefbeheer/config/tests/test_views.py @@ -1,5 +1,8 @@ +from unittest.mock import patch + from django.test import override_settings, tag +from mozilla_django_oidc_db.models import OpenIDConnectConfig from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase @@ -93,3 +96,35 @@ def test_can_send_empty_list(self): config = ArchiveConfig.get_solo() self.assertEqual(config.zaaktypes_short_process, []) + + +class OIDCInfoViewTests(APITestCase): + def test_oidc_info_view_not_enabled(self): + config_url = reverse("api:oidc-info") + with patch( + "openarchiefbeheer.config.api.views.OpenIDConnectConfig.get_solo", + return_value=OpenIDConnectConfig(enabled=False), + ): + response = self.client.get(config_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertFalse(data["enabled"]) + self.assertEqual(data["loginUrl"], "") + + def test_oidc_info_view_enabled(self): + config_url = reverse("api:oidc-info") + with patch( + "openarchiefbeheer.config.api.views.OpenIDConnectConfig.get_solo", + return_value=OpenIDConnectConfig(enabled=True), + ): + response = self.client.get(config_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertTrue(data["enabled"]) + self.assertEqual(data["loginUrl"], "http://testserver/oidc/authenticate/") diff --git a/backend/src/openarchiefbeheer/fixtures/oidc_config_test.json b/backend/src/openarchiefbeheer/fixtures/oidc_config_test.json new file mode 100644 index 00000000..c43a7dc5 --- /dev/null +++ b/backend/src/openarchiefbeheer/fixtures/oidc_config_test.json @@ -0,0 +1,44 @@ +[ + { + "model": "mozilla_django_oidc_db.openidconnectconfig", + "pk": 1, + "fields": { + "enabled": true, + "oidc_rp_client_id": "openarchiefbeheer-dev", + "oidc_rp_client_secret": "bla", + "oidc_rp_sign_algo": "RS256", + "oidc_rp_scopes_list": "[\"openid\", \"email\", \"profile\"]", + "oidc_op_discovery_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/", + "oidc_op_jwks_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/protocol/openid-connect/certs", + "oidc_op_authorization_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/protocol/openid-connect/auth", + "oidc_op_token_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/protocol/openid-connect/token", + "oidc_token_use_basic_auth": false, + "oidc_op_user_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/protocol/openid-connect/userinfo", + "oidc_rp_idp_sign_key": "", + "oidc_op_logout_endpoint": "http://localhost:28080/realms/openarchiefbeheer-dev/protocol/openid-connect/logout", + "oidc_use_nonce": true, + "oidc_nonce_size": 32, + "oidc_state_size": 32, + "oidc_keycloak_idp_hint": "", + "userinfo_claims_source": "userinfo_endpoint", + "username_claim": "[\"preferred_username\"]", + "claim_mapping": { + "email": [ + "email" + ], + "last_name": [ + "family_name" + ], + "first_name": [ + "given_name" + ] + }, + "groups_claim": "[\"resource_access\", \"openarchiefbeheer-dev\", \"roles\"]", + "sync_groups": true, + "sync_groups_glob_pattern": "*", + "make_users_staff": true, + "superuser_group_names": "[\"Superuser\"]", + "default_groups": [] + } + } +] \ No newline at end of file diff --git a/backend/src/openarchiefbeheer/templates/maykin_2fa/login.html b/backend/src/openarchiefbeheer/templates/maykin_2fa/login.html index 93586c74..60c794dd 100644 --- a/backend/src/openarchiefbeheer/templates/maykin_2fa/login.html +++ b/backend/src/openarchiefbeheer/templates/maykin_2fa/login.html @@ -1,6 +1,5 @@ {% extends "maykin_2fa/login.html" %} -{% load i18n %} - +{% load i18n solo_tags %} {% block footer %} @@ -8,7 +7,13 @@ {% block extra_login_options %} -{#Include additional (OIDC) authentication options here #} +{% get_solo 'mozilla_django_oidc_db.OpenIDConnectConfig' as oidc_config %} + +{% if oidc_config.enabled %} +
+ {% trans "Login with OIDC" %} +
+{% endif %} {% endblock %} diff --git a/backend/src/openarchiefbeheer/urls.py b/backend/src/openarchiefbeheer/urls.py index a8d38347..e9bac69d 100644 --- a/backend/src/openarchiefbeheer/urls.py +++ b/backend/src/openarchiefbeheer/urls.py @@ -9,6 +9,7 @@ from maykin_2fa import monkeypatch_admin from maykin_2fa.urls import urlpatterns, webauthn_urlpatterns +from mozilla_django_oidc_db.views import AdminLoginFailure from openarchiefbeheer.accounts.views.password_reset import PasswordResetView @@ -39,6 +40,7 @@ path("admin/", include((webauthn_urlpatterns, "two_factor"))), path("admin/hijack/", include("hijack.urls")), path("admin/", admin.site.urls), + path("admin/login/failure/", AdminLoginFailure.as_view(), name="admin-oidc-error"), path( "reset///", auth_views.PasswordResetConfirmView.as_view(), @@ -49,6 +51,7 @@ auth_views.PasswordResetCompleteView.as_view(), name="password_reset_complete", ), + path("oidc/", include("mozilla_django_oidc.urls")), path("api/", include("openarchiefbeheer.api.urls", namespace="api")), # Simply show the master template. path("", TemplateView.as_view(template_name="master.html"), name="root"), diff --git a/backend/src/openarchiefbeheer/utils/tests/e2e.py b/backend/src/openarchiefbeheer/utils/tests/e2e.py index 0302d6e3..4f9e0f69 100644 --- a/backend/src/openarchiefbeheer/utils/tests/e2e.py +++ b/backend/src/openarchiefbeheer/utils/tests/e2e.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core.cache import cache +from django.test.testcases import LiveServerThread, QuietWSGIRequestHandler from playwright.async_api import async_playwright @@ -44,9 +45,28 @@ async def browser_page_with_tracing(): await browser.close() +class LiveServerThreadWithReuse(LiveServerThread): + """Live server thread with reuse of local addresses + + Apparently, after the server thread is stopped, the socket is still bound to the address and in TIME_WAIT state. + The connection is kept around so that any delayed packets can be matched to the connection and handled appropriately. + The OS will close the connection once a timeout period has passed. + By reusing the address, we prevent the ``socket.error: [Errno 48] Address already in use`` error. + """ + + def _create_server(self, connections_override=None): + return self.server_class( + (self.host, self.port), + QuietWSGIRequestHandler, + allow_reuse_address=True, + connections_override=connections_override, + ) + + class PlaywrightTestCase(StaticLiveServerTestCase): port = settings.E2E_PORT fixtures = ["permissions.json"] + server_thread_class = LiveServerThreadWithReuse def setUp(self): super().setUp() diff --git a/docker-compose.yml b/docker-compose.yml index eadc85cb..58719baf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,10 @@ # # DISCLAIMER: THIS IS FOR DEVELOPMENT PURPOSES ONLY AND NOT SUITABLE FOR PRODUCTION. # -version: '3' +# You can use this docker-compose to spin up a local stack for demo/try-out +# purposes, or to get some insight in the various components involved (e.g. to build +# your Helm charts from). Note that various environment variables are UNSAFE and merely +# specified so that you can get up and running with the least amount of friction. services: db: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 38d6afe7..65d4d3c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "@maykin-ui/admin-ui": "^0.0.30", + "@maykin-ui/admin-ui": "^0.0.33", "@storybook/test-runner": "^0.18.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", @@ -3665,9 +3665,9 @@ "license": "MIT" }, "node_modules/@maykin-ui/admin-ui": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@maykin-ui/admin-ui/-/admin-ui-0.0.30.tgz", - "integrity": "sha512-DBAA5GY576z1uSVb3zWlsHJGz6HaSK+XHWyLz2eiCTcR5/SifMMIj82HZqT7Afb8K3X6PjuoBomuO5NsK93Htg==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@maykin-ui/admin-ui/-/admin-ui-0.0.33.tgz", + "integrity": "sha512-F2jFOxDI+SWteqq3RiuvOsDBWgibIa9CoC+SZwmvPri4vyt2sV5OJoaI0WYIdtzcDdZClkolhLo5xDzQqf7Tzg==", "dependencies": { "@floating-ui/react": "^0.26.6", "@heroicons/react": "^2.1.1", diff --git a/frontend/package.json b/frontend/package.json index a8e16ce5..593beeff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@maykin-ui/admin-ui": "^0.0.30", + "@maykin-ui/admin-ui": "^0.0.33", "@storybook/test-runner": "^0.18.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b0ca5e14..383eabdf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,7 +27,10 @@ import { import { useAsync } from "react-use"; import "./App.css"; -import { User, whoAmI } from "./lib/api/auth"; +import { User, getOIDCInfo, whoAmI } from "./lib/api/auth"; +import OidcConfigContext, { + OidcConfigContextType, +} from "./lib/contexts/OidcConfigContext"; import { formatUser } from "./lib/format/user"; function App() { @@ -38,12 +41,21 @@ function App() { const handle = match?.handle as Record; const [user, setUser] = useState(null); + const [oidcInfo, setOidcInfo] = useState({ + enabled: false, + loginUrl: "", + }); useAsync(async () => { const user = await whoAmI(); setUser(user); }, [state]); + useAsync(async () => { + const info = await getOIDCInfo(); + setOidcInfo(info); + }, [state]); + const breadcrumbItems = ( (handle?.breadcrumbItems || []) as BreadcrumbItem[] ).map((b) => ({ @@ -150,9 +162,11 @@ function App() { ], }} > - - - + + + + + ); diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts index 86d37a79..d40ad2c1 100644 --- a/frontend/src/lib/api/auth.ts +++ b/frontend/src/lib/api/auth.ts @@ -1,4 +1,5 @@ import { cacheDelete, cacheMemo } from "../cache/cache"; +import { OidcConfigContextType } from "../contexts/OidcConfigContext"; import { request } from "./request"; export type User = { @@ -46,3 +47,14 @@ export async function whoAmI() { return promise; }); } + +/** + * API call to get info about OIDC. + */ +export async function getOIDCInfo() { + return cacheMemo("getOIDCInfo", async () => { + const response = await request("GET", "/oidc-info"); + const promise: Promise = response.json(); + return promise; + }); +} diff --git a/frontend/src/lib/contexts/OidcConfigContext.ts b/frontend/src/lib/contexts/OidcConfigContext.ts new file mode 100644 index 00000000..c7747a70 --- /dev/null +++ b/frontend/src/lib/contexts/OidcConfigContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; + +export type OidcConfigContextType = { + enabled: boolean; + loginUrl: string; +}; + +const OidcConfigContext: React.Context = createContext({ + enabled: false, + loginUrl: "", +} as OidcConfigContextType); + +export default OidcConfigContext; diff --git a/frontend/src/pages/login/Login.stories.tsx b/frontend/src/pages/login/Login.stories.tsx index fe57483a..a1e62510 100644 --- a/frontend/src/pages/login/Login.stories.tsx +++ b/frontend/src/pages/login/Login.stories.tsx @@ -1,7 +1,9 @@ import "@maykin-ui/admin-ui/style"; import type { Meta, StoryObj } from "@storybook/react"; +import { expect, within } from "@storybook/test"; import { ReactRouterDecorator } from "../../../.storybook/decorators"; +import OidcConfigContext from "../../lib/contexts/OidcConfigContext"; import { LoginPage } from "./Login"; const meta: Meta = { @@ -18,3 +20,29 @@ export const LoginPageStory: Story = { children: "The quick brown fox jumps over the lazy dog.", }, }; + +export const LoginPageWithOIDC: Story = { + render: (args) => ( + + + + ), + play: async (context) => { + const canvas = within(context.canvasElement); + + const oidcButton: HTMLBaseElement = await canvas.findByRole("link", { + name: "Organisatie login", + }); + + const redirectUrl = new URL(oidcButton.href); + const nextUrl = redirectUrl.searchParams.get("next"); + + expect(nextUrl).not.toBeNull(); + expect(new URL(nextUrl as string).pathname).toEqual("/"); + }, +}; diff --git a/frontend/src/pages/login/Login.tsx b/frontend/src/pages/login/Login.tsx index 3b21cf82..f597223c 100644 --- a/frontend/src/pages/login/Login.tsx +++ b/frontend/src/pages/login/Login.tsx @@ -1,16 +1,38 @@ -import { AttributeData, LoginTemplate, forceArray } from "@maykin-ui/admin-ui"; +import { + AttributeData, + LoginTemplate, + LoginTemplateProps, + forceArray, +} from "@maykin-ui/admin-ui"; +import { useContext } from "react"; import { useActionData, useSubmit } from "react-router-dom"; +import OidcConfigContext from "../../lib/contexts/OidcConfigContext"; import "./Login.css"; export type LoginProps = React.ComponentProps<"main"> & { // Props here. }; +/* + * Add the redirect URL to the callback URL + */ +const makeRedirectUrl = (oidcLoginUrl: string) => { + const currentUrl = new URL(window.location.href); + const redirectUrl = new URL("/", currentUrl); + const loginUrl = new URL(oidcLoginUrl); + loginUrl.searchParams.set("next", redirectUrl.href); + + return loginUrl.href; +}; + /** * Login page */ export function LoginPage({ ...props }: LoginProps) { + const { enabled: oidcEnabled, loginUrl: oidcLoginUrl } = + useContext(OidcConfigContext); + const fields = [ { autoFocus: true, @@ -38,6 +60,12 @@ export function LoginPage({ ...props }: LoginProps) { ); const { detail, nonFieldErrors, ...errors } = formErrors; + const oidcProps: Partial = {}; + if (oidcEnabled) { + oidcProps.urlOidcLogin = makeRedirectUrl(oidcLoginUrl); + oidcProps.labelOidcLogin = "Organisatie login"; + } + return ( } // FIXME: Should be easier to override @@ -48,6 +76,7 @@ export function LoginPage({ ...props }: LoginProps) { onSubmit: (_, data) => submit(data as AttributeData, { method: "POST" }), }} + {...oidcProps} {...props} /> );