{"id":18888160,"url":"https://github.com/consol/keycloak-webinar","last_synced_at":"2026-02-25T16:31:26.370Z","repository":{"id":72844616,"uuid":"278096953","full_name":"ConSol/keycloak-webinar","owner":"ConSol","description":null,"archived":false,"fork":false,"pushed_at":"2020-07-08T13:38:05.000Z","size":2200,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-05-31T12:30:37.176Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ConSol.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-07-08T13:29:04.000Z","updated_at":"2020-07-08T13:38:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"bfc6c068-3ec7-464b-8922-5f16058653e7","html_url":"https://github.com/ConSol/keycloak-webinar","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ConSol/keycloak-webinar","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ConSol%2Fkeycloak-webinar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ConSol%2Fkeycloak-webinar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ConSol%2Fkeycloak-webinar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ConSol%2Fkeycloak-webinar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ConSol","download_url":"https://codeload.github.com/ConSol/keycloak-webinar/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ConSol%2Fkeycloak-webinar/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29830106,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T15:41:19.027Z","status":"ssl_error","status_checked_at":"2026-02-25T15:40:47.150Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T07:42:33.475Z","updated_at":"2026-02-25T16:31:26.351Z","avatar_url":"https://github.com/ConSol.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Consol Webinar: Application Security with OAuth2 and Keycloak\n\n## What we will do\nWe start by scratch with a blank keycloak docker image. Throughout this session, we will\n- Create and understand realms\n- Create clients\n- Create users\n- Query and verify tokens through REST calls\n- Define client-specific mappings\n- Understand and create roles\n- Understand the use-case for groups, create groups\n\n## Out of scope\n- A discussion of default clients and roles provided by keycloak\n- A discussion of different OAuth-flows\n- The use of refresh tokens\n- How to use JWT Tokens in the backend system for Authorization\n\n## Tools needed\n- A functional docker installation\n- Internet connection (for the initial pull of the docker images)\n- A tool to execute REST calls. We will use the command-line tool [`curl`][curl], but one can \nachieve the same with, for example, [Insomnia][insomnia] or [Postman][postman].\n\n## Starting the docker deployment\n\nTo start playing around, we need a running keycloak instance and a database as backing persistence \nprovider. For this, we have created a docker-compose file in the [`./docker`][dockerFolder] folder.\nThe deployment can be started by changing into the directory and executing `docker-compose`:\n\n    cd ./docker\n    docker-compose up -d\n\nThe initial download of the docker image can take some time, so feel free to grab a coffee.\n\nAfter the images have been pulled, keycloak itself takes some time to start up. We can check the \nstatus of the container by executing\n\n    docker ps\n    CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                              NAMES\n    98fd842caeea        jboss/keycloak:10.0.2   \"/opt/jboss/tools/do…\"   14 minutes ago      Up 14 minutes       8443/tcp, 0.0.0.0:8090-\u003e8080/tcp   docker_keycloak-service_1\n    12f40ec3282e        postgres:12.2           \"docker-entrypoint.s…\"   14 minutes ago      Up 14 minutes       5432/tcp                           docker_postgres-service_1\n\nand then showing the log of the keycloak container:\n\n    docker logs -f 9\n    ...\n    07:36:09,979 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 10.0.2 (WildFly Core 11.1.1.Final) started in 21244ms - Started 690 of 995 services (708 services are lazy, passive or on-demand)\n\nWhen we see the above log message, that means keycloak is up and running, ready to be configured.\n\n## Logging in to the web UI\nThe keycloak container is configured to listen on local port 8090, so accessing \n[http://localhost:8090][localKeycloak]. We are greeted by the keycloak login page:\n\n![KeycloakHome][keycloakHome]\n\nA click on `Administration Console` brings us to the actual login:\n\n![KeycloakLogin][keycloakLogin]\n\nThe keycloak is configured with the following credentials:\n\n![KeycloakAdminConsole][keycloakAdminConsole]\n\n- Username: `keycloak`\n- Password: `keycloak`\n\nAfter the successful login, we see the adminstrative web ui:\n\n## Realms\nRealms are a means to coarsely subdivide your userbase.\n\nAs we can see on the upper left, we are currently on the `Master` realm. This is a special realm to \nmanage the keycloak configuration. To manage our users, we want to create a new realm. We can \nachieve this by hovering over `Master`, revealing a menu to `Add Realm`.\n\n![keycloakNewRealm][keycloakNewRealm]\n\nFor this demo, we will call our realm `app-realm`. After clicking on `Create`, the realm will be \ncreated and its base configuration opened:\n\n![keycloakRealmConfig][keycloakRealmConfig]\n\nHere, we can configure the newly created realm. For example, under the tab `Login`, we can configure\nwhether users are able to create new accounts or reset their password.\n\n## Clients\nClients are entities that use the keycloak server to get authentication information, e.g. self-owned\nmicroservices or other websites. To limit access to the authentication information, the access is \nrestricted. Whenever a services wants to interact with keycloak, it must provide a client id and, \ndepending on the operation, a secret to identify itself. Keycloak comes with some clients pre-defined \nthat are necessary for keycloak to fuction. We can see the clients by clicking on `Clients` under \n`Configs` in the left menu:\n\n![keycloakClients][keycloakClients]\n\n\nWe will ignore the existing clients for now and instead create two new clients `serviceOne` and \n`serviceTwo`:\n\n![keycloakClients][keycloakClients]\n\nThe two roles will represent two different services that need different user information. We will \nsee later on how we can customize those information, based on the client.\n\nSince we are going to use the `password grant` for this demonstration, we need to configure both \nnewly create clients to allow them this grant. \n\nIn the detail view of the client, weswitch the `Access Type` to `confidential`, disable the \nStandard Flow and click `Save` on the bottom of the page.\n\n![keycloakConfigureClient][keycloakConfigureClient]\n\nAfter saving, a new tab is `Credentials` is visible. We switch to that tab and write down the \nsecret, we will use it later on.\n\nRepeat the process for both clients.\n\n## Users, Groups and Roles\n\n### Users\nThe core concept of authentication are users and roles. A user represents an entity interacting with \nthe services - a human being. We can create a user by clicking on the menu `Users` under `Manage` on\nthe left of the screen.\n\n![keycloakUsers][keycloakUsers]\n\nThe list show will always be empty. If we had created some users already, we would have to click the\nbutton `View all users`.\n\nTo create a new user, we simply click `Add user` on the upper right: \n\n![keycloakAddUser][keycloakAddUser]\n\nWe just fill in the username as `alice` and then click `Save`. This will create the user and show an\noverview of the user. Notice, however that we have not yet defined a password for the user. To set a \npassword, we click on the tab `Credentials` and set a new password (`password`) for alice. We want \nthe password to not be temporary, so we have to click the matching toggle button:\n\n![setUserPassword][setUserPassword]\n\n#### Fetch the first token\nNow that we have our first user, we can try to fetch our first token by executing a `curl` command \nfrom the console. Please remember to replace `\u003cserviceOneSecretHere\u003e` with the actual secret of \nclient `serviceOne`.\n\n    curl \\\n      -k \\\n      --data \"grant_type=password\u0026client_id=serviceOne\u0026client_secret=\u003cserviceOneSecretHere\u003e\u0026username=alice\u0026password=password\" \\\n      http://localhost:8090/auth/realms/app-realm/protocol/openid-connect/token\n\nThe response should look something like this:\n\n    {\n        \"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5M1NoMFl6anFBU0xYWUYwdmxuQ29IamhjZTl1VVlnV3VrSk1leWxNQlpFIn0.eyJleHAiOjE1OTI1NjIxMTgsImlhdCI6MTU5MjU2MTgxOCwianRpIjoiMGJlNWVmNTctNWY4My00NDJkLWFkNjEtNTc3OTRkMTUxOTkxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDkwL2F1dGgvcmVhbG1zL2FwcC1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJjZDJhYzY1YS01NzY5LTQ5NjItOTc4Zi1hNTFhYjU5YWRlZjciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzZXJ2aWNlT25lIiwic2Vzc2lvbl9zdGF0ZSI6IjQ3OGMxMWIwLTUzZDUtNDA5MC1hODM5LWI2Yzk0NDUzN2M4OCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxpY2UifQ.T5EUOr0vEYyMC2nMbs5cc3XD3Lh8Ek6pYyga7PI88lL13Z-vK_a4znxKYxnMabZZGzjNI6hM1VBiUFIrfoqCQkEpfeN4Ye2QQpfy4XeH6--_830cYcSY7SBhdxD0uE3w84gHiPApieMlwsY2MQ6sSdr65OnH8odqEtBNpRhfEOP-FkpXoNrHTE4B8cKIsEYt9csf27ykAmqYDvHcyzJK6uJq9IQ5PekBYlAguAt6vEuEDrVSHddDUbjmL7csMpqM_D6yeklh9gZbnNKeLOOdxvNXdda-p62KM69wId4OjBAf36-a7dOXaszEUSWaK9l-Xahhp3_LCbhjAlZ-DFGdhw\",\n        \"expires_in\":300,\n        \"refresh_expires_in\":1800,\n        \"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhMWM4ZTRhNy0zNDNlLTRiOTItYWI5Yy1mMzg4YTlmMDFhM2IifQ.eyJleHAiOjE1OTI1NjM2MTgsImlhdCI6MTU5MjU2MTgxOCwianRpIjoiMmU5MTQzYTgtYTAxNC00NjE0LThjYTktMDNiMzg1OTcwYmFmIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDkwL2F1dGgvcmVhbG1zL2FwcC1yZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA5MC9hdXRoL3JlYWxtcy9hcHAtcmVhbG0iLCJzdWIiOiJjZDJhYzY1YS01NzY5LTQ5NjItOTc4Zi1hNTFhYjU5YWRlZjciLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoic2VydmljZU9uZSIsInNlc3Npb25fc3RhdGUiOiI0NzhjMTFiMC01M2Q1LTQwOTAtYTgzOS1iNmM5NDQ1MzdjODgiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUifQ.6AyyRT4b0YNRasIKzLt5yIzA212-2c6e_fyliNJ7nCE\",\n        \"token_type\":\"bearer\",\n        \"not-before-policy\":0,\n        \"session_state\":\"478c11b0-53d5-4090-a839-b6c944537c88\",\n        \"scope\":\"email profile\"\n    }\n\n(The response shown here is formatted for clarity)\n\nWe see that the response includes two tokens, one called `access_token` and one called \n`bearer_token`, as well as some meta-inforation.\n\nThe `access_token` is the actual authentication provider. It is a Base64-encoded string. We can\ndecode it either manually or let the website [jwt.io][jwt.io] do the decoding for us.\n\nThe payload-part of jwt.io is the interesting bit. It shows the following information:\n\n    {\n      \"exp\": 1592562118,\n      \"iat\": 1592561818,\n      \"jti\": \"0be5ef57-5f83-442d-ad61-57794d151991\",\n      \"iss\": \"http://localhost:8090/auth/realms/app-realm\",\n      \"aud\": \"account\",\n      \"sub\": \"cd2ac65a-5769-4962-978f-a51ab59adef7\",\n      \"typ\": \"Bearer\",\n      \"azp\": \"serviceOne\",\n      \"session_state\": \"478c11b0-53d5-4090-a839-b6c944537c88\",\n      \"acr\": \"1\",\n      \"realm_access\": {\n        \"roles\": [\n          \"offline_access\",\n          \"uma_authorization\"\n        ]\n      },\n      \"resource_access\": {\n        \"account\": {\n          \"roles\": [\n            \"manage-account\",\n            \"manage-account-links\",\n            \"view-profile\"\n          ]\n        }\n      },\n      \"scope\": \"email profile\",\n      \"email_verified\": false,\n      \"preferred_username\": \"alice\"\n    }\n\nEach key-value pair of this JSON-object is called a *claim*.\n\nWe can see when the token was issued (`iat`), when it expires (`exp`), the issuer of the token \n(`iss`), the client that made the request (`azp`) and the entity authenticated by this token \n(`preferred_username`), along with furhter meta-information. If we now, for example, add an email \naddress to `alice` and then request a new token, the token will then include the email address:\n\n    {\n      ...\n      \"email\": \"alice@wonder.land\"\n    }\n\nFurthermore, we can see that there are two `roles`-objects: the one is `realm_access.roles`, the \nother is `resource_access.account.roles`. `realm_access.roles` lists the roles of the entity \nauthenticated (alice), while `resource_access.account.roles` shows the roles of the client that made\nthe query.\n\n#### Modifying the token\n\nSuppose that we do not want the tokens to include the client roles. How do we do that? The \nattributes stored in keycloak must be mapped to token claims. This done through *mappers*. Mappers, \nin return, are bound to clients or client scopes. Client scopes can be bound to clients.\n\nInspecting the mappers for client `serviceOne` reveals that this client does not have any mapper \nassociated:\n\n![keycloakClientMapper][keycloakClientMapper]\n\nBut `serviceOne` has some client scopes associated:\n\n![keycloakClientScope][keycloakClientScope]\n\nIn particular, we see the `roles` scope associated.\n\nInspecting the mappers associated with the client scope `roles` shows a mapper `client roles`:\n\n![keycloakClientScopeMapper][keycloakClientScopeMapper]\n\nDeleting this mapper from the client scope and issuing a new token will give us the intended result:\n\n    {\n      \"exp\": 1592564972,\n      \"iat\": 1592564672,\n      \"jti\": \"04c12559-da70-4578-8611-aae6ab747825\",\n      \"iss\": \"http://localhost:8090/auth/realms/app-realm\",\n      \"aud\": \"account\",\n      \"sub\": \"cd2ac65a-5769-4962-978f-a51ab59adef7\",\n      \"typ\": \"Bearer\",\n      \"azp\": \"serviceOne\",\n      \"session_state\": \"d7477175-21b8-4b15-837c-91af7b050a73\",\n      \"acr\": \"1\",\n      \"realm_access\": {\n        \"roles\": [\n          \"offline_access\",\n          \"uma_authorization\"\n        ]\n      },\n      \"scope\": \"email profile\",\n      \"email_verified\": false,\n      \"preferred_username\": \"alice\",\n      \"email\": \"alice@wonder.land\"\n    }\n\nThis removes the claim `resource_access.account.roles` for *all* clients. But what if we want to \nonly modify information for *one specific* client? In this case, we can add our own mapper. \nSuppose, for example, that for `serviceOne`, we want to add the value `service.one.example.com` to \nthe audience (`aud`) claim to identify that only this service should use the token. For this, we \nnavigate to the mappers of client `serviceOne` and click the `Create` button on the upper right. We\ndefine the mapper to add the value we want to the audience claim:\n\n![keycloakAddMapperToClient][keycloakAddMapperToClient]\n\nFetching and inspecting a new token reveals that, in deed, the value has been added to the audience\nclaim:\n\n    {\n      ...\n      \"aud\": [\n        \"service.one.example.com\",\n        \"account\"\n      ],\n      ...\n    }\n\nIf we, however, issue a token for client `serviceTwo` with the following command (remember to \nreplace `\u003cserviceTwoSecretHere\u003e` with the actual secret):\n\n    curl \\\n      -k \\\n      --data \"grant_type=password\u0026client_id=serviceTwo\u0026client_secret=2f2d5ef2-875a-48d2-b1d4-1a905ae32312\u0026username=alice\u0026password=password\" \\\n      http://localhost:8090/auth/realms/app-realm/protocol/openid-connect/token\n\nand inspect this token, we see that `\"service.one.example.com\"` is missing from the audience claim:\n\n    {\n      \"exp\": 1592565846,\n      \"iat\": 1592565546,\n      \"jti\": \"84bcd0ff-87a3-45cd-b405-f4a6eb826369\",\n      \"iss\": \"http://localhost:8090/auth/realms/app-realm\",\n      \"aud\": \"account\",\n      \"sub\": \"cd2ac65a-5769-4962-978f-a51ab59adef7\",\n      \"typ\": \"Bearer\",\n      \"azp\": \"serviceTwo\",\n      \"session_state\": \"e811a9d1-e0af-403f-b3d1-054925cc8843\",\n      \"acr\": \"1\",\n      \"realm_access\": {\n        \"roles\": [\n          \"offline_access\",\n          \"uma_authorization\"\n        ]\n      },\n      \"scope\": \"email profile\",\n      \"email_verified\": false,\n      \"preferred_username\": \"alice\",\n      \"email\": \"alice@wonder.land\"\n    }\n    \nThus, we have a client-dependent token mapping.\n\n### Roles\n\nRoles define which actions a user is allowed to execute. These information is used by the backend \nsystem to allow/deny actions or access to web resources. \n\nalice already has some roles (`\"offline_access\"` and `\"uma_authorization\"`) associated. We can add\nnew roles by clicking on the corresponding menu on the left, and then clicking `Add Role`: \n\n![keycloakRoles][keycloakRoles]\n\nLet's keep things simple and add two roles: `User` and `Admin`. Then, let us add those roles to the \nuser `alice`:\n\n![keycloakUserAddRole][keycloakUserAddRole] \n\nIf we now request and inspect a new token for alice, we see that both roles are added to the token:\n\n    {\n      ...\n      \"realm_access\": {\n        \"roles\": [\n          \"User\",\n          \"offline_access\",\n          \"uma_authorization\",\n          \"Admin\"\n        ]\n      },\n      ...\n    }\n\n### Groups\nIf we have some more roles or maybe dependencies between roles (e.g. \"each `Admin` should also be a \n`User`), we can create groups, assign the roles to the group and then assign the group to a user. \nWhat is more: we can define default groups that are always added to a new user. This gives us an \nelegant solution to manage complex role structures.\n\nWe start by adding two Groups `Users` and `Admins`. Next, we add the roles `User` and `Admin` to the \ngroup `Admins`:\n\n![keycloakGroupAddRoles][keycloakGroupAddRoles]\n\nFor the group `Users`, we only add the role `User`.\n\nNext, we define the group `Users` as default group:\n\n[keycloakDefaultGroups][keycloakDefaultGroups]\n\nIf we now create a new user `bob`, the new user will have the group `Users` associated:\n\n![keycloakUserGroups][keycloakUserGroups]\n\nand what's more, `bob` also inherited the roles from the group:\n\n![keycloakUserRoles][keycloakUserRoles]\n\nAnd, of course, if we now issue a token for `bob` (we have to set passwort for `bob` first, though),\nthe roles will be visible in the token:\n\n    {\n      \"exp\": 1592567504,\n      \"iat\": 1592567204,\n      \"jti\": \"7e690009-3cf9-43fe-a39c-c6f3fcc52453\",\n      \"iss\": \"http://localhost:8090/auth/realms/app-realm\",\n      \"aud\": [\n        \"service.one.example.com\",\n        \"account\"\n      ],\n      \"sub\": \"f2b7eb22-b0d8-4c83-bf35-101cba3ed1e7\",\n      \"typ\": \"Bearer\",\n      \"azp\": \"serviceOne\",\n      \"session_state\": \"dbab8222-d214-4e60-94d4-89d8e2f844f0\",\n      \"acr\": \"1\",\n      \"realm_access\": {\n        \"roles\": [\n          \"User\",\n          \"offline_access\",\n          \"uma_authorization\"\n        ]\n      },\n      \"scope\": \"email profile\",\n      \"email_verified\": false,\n      \"preferred_username\": \"bob\"\n    }\n\n[curl]: https://curl.haxx.se/\n[insomnia]: https://insomnia.rest/\n[postman]: https://www.postman.com/\n[dockerFolder]: ./docker\n[localKeycloak]: http://localhost:8090/auth\n[keycloakHome]: ./images/KeycloakHome.png\n[keycloakLogin]: ./images/KeycloakLogin.png\n[keycloakAdminConsole]: ./images/KeycloakAdminConsole.png\n[keycloakNewRealm]: ./images/KeycloakNewRealm.png\n[keycloakRealmConfig]: ./images/KeycloakRealmConfig.png\n[keycloakClients]: ./images/KeycloakClients.png \n[keycloakUsers]: ./images/KeycloakUsers.png\n[keycloakAddUser]: ./images/KeycloakAddUser.png\n[setUserPassword]: ./images/SetUserPassword.png\n[keycloakClients]: ./images/KeycloakNewClient.png\n[keycloakConfigureClient]: ./images/KeycloakConfigureClient.png\n[jwt.io]: https://jwt.io/\n[keycloakClientMapper]: ./images/KeycloakClientMapper.png\n[keycloakClientScope]: ./images/KeycloakClientScope.png\n[keycloakClientScopeMapper]: ./images/KeycloakClientScopeMapper.png\n[keycloakAddMapperToClient]: ./images/KeycloakAddMapperToClient.png\n[keycloakRoles]: ./images/KeycloakRoles.png\n[keycloakUserAddRole]: ./images/KeycloakUserAddRole.png\n[keycloakGroupAddRoles]: ./images/KeycloakGroupAddRoles.png\n[keycloakDefaultGroups]: ./images/KeycloakDefaultGroups.png\n[keycloakUserGroups]: ./images/KeycloakUserGroups.png\n[keycloakUserRoles]: ./images/KeycloakUserRoles.png","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconsol%2Fkeycloak-webinar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fconsol%2Fkeycloak-webinar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconsol%2Fkeycloak-webinar/lists"}