{"id":46,"date":"2020-10-07T10:00:42","date_gmt":"2020-10-07T00:00:42","guid":{"rendered":"http:\/\/localhost:8000\/?p=46"},"modified":"2020-10-07T10:00:42","modified_gmt":"2020-10-07T00:00:42","slug":"oauth2-sso-keycloak-spring-security","status":"publish","type":"post","link":"http:\/\/www.cheerfulprogramming.com\/?p=46","title":{"rendered":"Oauth2 SSO Keycloak Spring Security"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>In this article we will set up a Keycloak Oauth2 Single Sign-On (SSO)<br \/>\nserver with two clients and two user accounts with differing<br \/>\npermissions. We will create a Spring Boot Java application (a \\&quot;resource<br \/>\nserver\\&quot;) with two REST endpoints, secured by the Keycloak server and<br \/>\nrequiring different roles for access. We will send HTTP requests to the<br \/>\nKeycloak server to create a valid Oauth2 token for each user, and use it<br \/>\nto invoke the REST endpoints in the Spring Boot resource server.<br \/>\nSecuring the communications between the parties with HTTPS is outside<br \/>\nthe scope of this article.<\/p>\n<p>You will need:<\/p>\n<ul>\n<li>A command line shell environment, such as Bash<\/li>\n<li>A Java development environment, such as OpenJDK11<\/li>\n<li>A text editor or Integrated Development Environment (IDE), for<br \/>\nauthoring code.<\/li>\n<li>A build tool such as Gradle or Maven.<\/li>\n<li>Tools for making HTTP requests, such as cURL, wget, or Postman.<\/li>\n<li>Tools for formatting JSON, such as jq (optional).<\/li>\n<li>Docker (optional)<\/li>\n<\/ul>\n<p>Some programming experience with Java or related technologies would be<br \/>\nmost beneficial.<\/p>\n<h2>Setting up a Keycloak Server<\/h2>\n<p>There are different ways to set up a Keycloak server as described on the<br \/>\nproject <a href=\"https:\/\/www.keycloak.org\/getting-started\">website<\/a>. In this<br \/>\nexample we will use Docker, but for the purpose of this article it<br \/>\ndoesn\\&#8217;t really matter which way you do it. To create and start the<br \/>\nserver using Docker, run (in a Bash shell):<\/p>\n<pre><code class=\"\" data-line=\"\">$ docker run --name keycloak -d -p 8082:8080 -p 8445:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io\/keycloak\/keycloak:11.0.0<\/code><\/pre>\n<p>This will expose the Keycloak server to listen to HTTP on 8082 and HTTPS<br \/>\non 8445. If you want to listen on other ports, just modify the command<br \/>\nabove. I chose these ports to minimise the chance of clashing with any<br \/>\nother servers running on my machine. If you are not familiar with<br \/>\nsetting up a realm on Keycloak, follow this<br \/>\n<a href=\"https:\/\/www.keycloak.org\/getting-started\/getting-started-docker\">tutorial<\/a><br \/>\non the Keycloak website to create a realm called myrealm, which is the<br \/>\nrealm we will use for the remainder of this article.<\/p>\n<h3>Create a Keycloak Client for your Spring Boot Resource Server<\/h3>\n<p>We will create a Keycloak Client called salutations-client:<\/p>\n<ol>\n<li>Go to myrealm<\/li>\n<li>Click Clients<\/li>\n<li>Click Create in the top-right of the page, as shown below:<br \/>\n<img decoding=\"async\" alt=\"MyRealm client screenshot\" src=\"\/wp-content\/uploads\/2023\/06\/Keycloak-myrealm-clients.png\" fluid\/><\/li>\n<li>Create a client called salutations-client as shown below:<br \/>\n<img decoding=\"async\" alt=\"MyRealm add salutations client screenshot\" src=\"\/wp-content\/uploads\/2023\/06\/Keycloak-myrealm-add-Salutations-client.png\" fluid\/><\/li>\n<li>Save it.<\/li>\n<li>Under Settings, set the Access Type to bearer-only, and the Admin<br \/>\nURL to <a href=\"https:\/\/localhost\">https:\/\/localhost<\/a>, as shown:<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Salutations-client-settings.png\" fluid\/><\/li>\n<li>Save it.<\/li>\n<li>Under Roles, select Add role. Give the new role a name, eg<br \/>\nspecial-user, and save it.<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Salutations-client-roles.png\" fluid\/><\/li>\n<\/ol>\n<h3>Create a Keycloak Client for User Authentication<\/h3>\n<p>We will create a Keycloak Client called salutations-client:<\/p>\n<ol>\n<li>Go to myrealm<\/li>\n<li>Click Clients<\/li>\n<li>Click Create in the top-right of the page.<\/li>\n<li>Create a client called salutations-session-create as shown below:<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Keycloak-myrealm-add-session-client.png\" fluid\/><\/li>\n<li>Save it.<\/li>\n<li>Under Settings, set the Access Type to openid-connect, the Root URL<br \/>\nto <a href=\"http:\/\/localhost\">http:\/\/localhost<\/a>, the Valid Redirect URIs to <a href=\"http:\/\/localhost\/\">http:\/\/localhost\/<\/a>*,<br \/>\nthe Admin URL to <a href=\"http:\/\/localhost\">http:\/\/localhost<\/a>, the Web Origins to<br \/>\n<a href=\"http:\/\/localhost\">http:\/\/localhost<\/a>, as shown:<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/sessions-client-settings.png\" fluid\/><\/li>\n<li>Save it.<\/li>\n<\/ol>\n<h3>Add Users and Role Mappings<\/h3>\n<ol>\n<li>Under Users, select Add user, and create a user with the username<br \/>\nuser. Feel free to choose a more interesting name here.<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Salutations-users-details.png\" fluid\/><\/li>\n<li>Set a password for your user, and set Temporary to OFF. For this<br \/>\ndemo, a password of password will suffice.<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Salutations-users-credentials.png\" fluid\/><\/li>\n<li>Under Role Mappings, select from the Client Roles dropdown the<br \/>\nsalutations-client client that you created in the steps above. Under<br \/>\nAvailable Roles, click on the special-user role that you created in<br \/>\nthe steps above. Click Add selected, to shift the role into the<br \/>\nAssigned Roles box. Now the user user has the special-user role.<br \/>\n<img decoding=\"async\" alt=\"\" src=\"\/wp-content\/uploads\/2023\/06\/Salutations-users-role-mappings.png\" fluid\/><\/li>\n<li>Repeat steps 1. and 2. for a new user called user2, but do not<br \/>\nassociate this user with any roles as for the user user.<\/li>\n<\/ol>\n<p>Now you are ready to create a Spring Boot resource server that will<br \/>\ngrant access to REST endpoints only if the user requesting them is<br \/>\nauthenticated and has the correct role.<\/p>\n<h2>Spring Configuration<\/h2>\n<p>Go to <a href=\"https:\/\/start.spring.io\/\">Spring Initializr<\/a> to create a new<br \/>\nSpring project. Add the following Spring dependencies before generating<br \/>\nand downloading the project:<\/p>\n<ol>\n<li>Oauth2 Resource Server<\/li>\n<li>Cloud Oauth2<\/li>\n<li>Spring Security<\/li>\n<li>Spring Web<\/li>\n<li>Spring Data JPA<\/li>\n<\/ol>\n<p>Add the following dependencies to your classpath, which I have shown in<br \/>\nGradle format:<\/p>\n<pre><code class=\"\" data-line=\"\">\/\/ https:\/\/mvnrepository.com\/artifact\/com.nimbusds\/nimbus-jose-jwt\ncompile     &#039;com.nimbusds:nimbus-jose-jwt:9.0.1&#039;\nruntimeOnly &#039;com.h2database:h2&#039;<\/code><\/pre>\n<p>Here is the application.properties file from the resources folder:<\/p>\n<pre><code class=\"\" data-line=\"\">## Oauth2 Configuration (Keycloak Auth Server)\nspring.security.oauth2.resourceserver.jwt.issuer-uri=https:\/\/localhost:8445\/auth\/realms\/myrealm\nspring.security.oauth2.resourceserver.jwt.jwk-set-uri=http:\/\/localhost:8082\/auth\/realms\/myrealm\/protocol\/openid-connect\/certs\nspring.security.oauth2.resource.prefer-token-info=true<\/code><\/pre>\n<h3>Spring Security<\/h3>\n<pre><code class=\"\" data-line=\"\">package com.cheerfulprogramming.edwhiting.keycloakdemo.config;\n\nimport org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.oauth2.jwt.Jwt;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;\n\nimport java.util.Collection;\n\n@Configuration\n@EnableOAuth2Sso\n@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)\npublic class WebSecurity extends WebSecurityConfigurerAdapter {\n    @Override\n    public void configure(HttpSecurity http) throws Exception {\n        http\n                .httpBasic().disable()\n                .formLogin().disable()\n                .logout().disable()\n                .cors().disable()\n                .csrf().disable()\n                .oauth2ResourceServer(oauth2ResourceServer -&gt;\n                        oauth2ResourceServer.jwt(jwt -&gt;\n                                jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))\n                .sessionManagement(sessionManagement -&gt;\n                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\n                .authorizeRequests()\n                    .antMatchers(HttpMethod.GET, &quot;\/api\/salutations\/**&quot;).permitAll()\n                    .anyRequest().authenticated();\n    }\n\n    private JwtAuthenticationConverter jwtAuthenticationConverter() {\n        JwtAuthenticationConverter j = new JwtAuthenticationConverter();\n        j.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());\n        return j;\n    }\n\n    private Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt; jwtGrantedAuthoritiesConverter() {\n        CustomKeycloakGrantedAuthoritiesConverter c = new CustomKeycloakGrantedAuthoritiesConverter(&quot;salutations-client&quot;);\n        c.setAuthorityPrefix(&quot;ROLE_&quot;);\n        return c;\n    }\n}<\/code><\/pre>\n<p>Here is the CustomKeycloakGrantedAuthoritiesConverter:<\/p>\n<pre><code class=\"\" data-line=\"\">package com.cheerfulprogramming.edwhiting.keycloakdemo.config;\n\nimport com.nimbusds.jose.shaded.json.JSONArray;\nimport com.nimbusds.jose.shaded.json.JSONObject;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.oauth2.jwt.Jwt;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\npublic class CustomKeycloakGrantedAuthoritiesConverter implements Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt; {\n\n    public static final String RESOURCE_ACCESS = &quot;resource_access&quot;;\n    public static final String ROLES = &quot;roles&quot;;\n\n    private final String clientId;\n    private String authorityPrefix = &quot;ROLE_&quot;;\n\n    public CustomKeycloakGrantedAuthoritiesConverter(final String clientId) {\n        this.clientId = clientId;\n    }\n\n    \/**\n      * This method extracts the elements of the &quot;roles&quot; array for the specified clientId, from the\n      * JWT token supplied by Keycloak when a user authenticates.  eg  where clientId is &quot;my-client-id&quot;,\n      * and the resource request contains the token below, the roles will be [&quot;user&quot;, &quot;admin&quot;].\n      * {\n      *  ...,\n      *  &quot;resource_access&quot;: {\n      *   &quot;my-client-id&quot;: {\n      *    &quot;roles&quot;: [\n      *     &quot;user&quot;,\n      *     &quot;admin&quot;\n      *    ]\n      *   },\n      *   ...\n      *  },\n      *  ...\n      * }\n      * @param source\n      * @return\n      *\/\n    @Override\n    public Collection&lt;GrantedAuthority&gt; convert(final Jwt source) {\n        return Optional.ofNullable(source.getClaim(RESOURCE_ACCESS)).stream()\n                .map(JSONObject.class::cast)\n                .flatMap(resourceAccessClaim -&gt; Optional.ofNullable(resourceAccessClaim.get(clientId)).stream())\n                .map(JSONObject.class::cast)\n                .flatMap(clientClaim -&gt; Optional.ofNullable(clientClaim.get(ROLES)).stream())\n                .map(JSONArray.class::cast)\n                .flatMap(roles -&gt; roles.stream())\n                .map(role-&gt; new SimpleGrantedAuthority(this.authorityPrefix + role))\n                .collect(Collectors.toCollection(ArrayList::new));\n    }\n\n    public CustomKeycloakGrantedAuthoritiesConverter setAuthorityPrefix(final String authorityPrefix) {\n        this.authorityPrefix = authorityPrefix;\n        return this;\n    }\n}<\/code><\/pre>\n<p>Here is an example controller with the special-user role enforced on one<br \/>\nof the endpoints.<\/p>\n<pre><code class=\"\" data-line=\"\">package com.cheerfulprogramming.edwhiting.keycloakdemo.controller.salutations;\n\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.security.core.annotation.AuthenticationPrincipal;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.security.Principal;\n\n@RestController(&quot;salutationsRestController&quot;)\n@RequestMapping(&quot;\/api\/salutations&quot;)\npublic class Controller {\n\n    \/**\n      * This method is open to any authenticated user.\n      *\/\n    @GetMapping(&quot;\/greeting&quot;)\n    public String greeting(@AuthenticationPrincipal JwtAuthenticationToken token) {\n        \/* You can inject the token as shown above and work with it if you wish *\/\n        return &quot;Hello Dave&quot;;\n    }\n\n    \/** \n      * This method is protected by Spring security so that only authenticated users \n      * with the &quot;special-user&quot; role can execute it.\n      *\/\n    @PreAuthorize(&quot;hasRole(&#039;special-user&#039;)&quot;)\n    @GetMapping(&quot;\/user&quot;)\n    public Principal user(Principal user) {\n        return user;\n    }\n}<\/code><\/pre>\n<h2>Test Your New Application<\/h2>\n<p>This section will demonstrate how to authenticate against the Keycloak<br \/>\nserver and call the Spring application with an HTTP request. In the real<br \/>\nworld, this is something that a browser-based JavaScript application<br \/>\nwould do. There is a <a href=\"https:\/\/www.keycloak.org\/docs\/latest\/securing_apps\/index.html#_javascript_adapter\">Keycloak JavaScript client<br \/>\nlibrary<\/a><br \/>\navailable for front-end applications, and a wrapper has been produced<br \/>\n<a href=\"https:\/\/www.npmjs.com\/package\/keycloak-angular\">for Angular<\/a>. For this<br \/>\nsection, however, we will not build a front-end, but we will invoke the<br \/>\nendpoints directly. You will need a tool that can send hand-crafted HTTP<br \/>\nmessages, such as <a href=\"https:\/\/curl.haxx.se\/\">cURL<\/a>,<br \/>\n<a href=\"https:\/\/www.gnu.org\/software\/wget\/\">wget<\/a> or<br \/>\n<a href=\"https:\/\/www.postman.com\/\">Postman<\/a>. You will also benefit from a JSON<br \/>\nformatter such as <a href=\"https:\/\/stedolan.github.io\/jq\/\">jq<\/a>. If you are<br \/>\nrunning a GNU\/Linux computing environment, your package manager (eg yum,<br \/>\napt, pacman) should have the ability to install at least some of these.<br \/>\nFor this example I use cURL and jq, in a Bash environment.<\/p>\n<p>The following code uses cURL to authenticate a user against the<br \/>\nsalutations-session-create Keycloak server and create a session token,<br \/>\nwhich I have put in a Bash script called keycloak-session-create.sh for<br \/>\nconvenience:<\/p>\n<pre><code class=\"\" data-line=\"\">#!\/bin\/bash\n\nfunction frontendToken() {\ncurl --request POST https:\/\/localhost:8445\/auth\/realms\/myrealm\/protocol\/openid-connect\/token \\\n    --header &#039;Content-Type: application\/x-www-form-urlencoded&#039; \\\n    --data-urlencode &#039;username=user&#039; \\\n    --data-urlencode &#039;password=password&#039; \\\n    --data-urlencode &#039;client_id=salutations-session-create&#039; \\\n    --data-urlencode &#039;grant_type=password&#039; \\\n    --insecure --silent --location \n}\n\nfrontendToken<\/code><\/pre>\n<p>Be sure to make your script executable:<\/p>\n<pre><code class=\"\" data-line=\"\">$ chmod 755 keycloak-session-create.sh<\/code><\/pre>\n<p>The best way to invoke this script and be able to read the results<br \/>\neasily is to pipe the results through jq, as shown:<\/p>\n<pre><code class=\"\" data-line=\"\">$ .\/keycloak-session-create.sh | jq<\/code><\/pre>\n<p>Now the useful datum that you need to include in any request to a<br \/>\nsecured end-point is stored in the access_token field. To be able to use<br \/>\nit in your next request easily, store it in a variable called TOKEN, as<br \/>\nshown (I have used tr to remove the quotes):<\/p>\n<pre><code class=\"\" data-line=\"\">$ TOKEN=$(.\/keycloak-session-create.sh | jq .access_token | tr -d &#039;&quot;&#039;)<\/code><\/pre>\n<p>Now is the time to run your Spring Boot application that you created<br \/>\nearlier. Assuming that it is listening on port 8080, once you have<br \/>\nauthenticated as shown above, you can make a request to a secured end<br \/>\npoint as shown below. Note that if your session has expired, then<br \/>\nenabling &#8211;verbose will show the HTTP 401 error message.<\/p>\n<pre><code class=\"\" data-line=\"\">$ curl --verbose \\\n    --header &quot;Authorization: Bearer $TOKEN&quot; \\\n    --header &quot;Content-Type: application\/json&quot; \\\n    http:\/\/localhost:8080\/api\/salutations\/greeting<\/code><\/pre>\n<p>Now test that your user has the role special-user and can access the<br \/>\nrestricted API endpoint in your application:<\/p>\n<pre><code class=\"\" data-line=\"\">$ curl --verbose \\\n    --header &quot;Authorization: Bearer $TOKEN&quot; \\\n    --header &quot;Content-Type: application\/json&quot; \\\n    http:\/\/localhost:8080\/api\/salutations\/user \\\n    | jq<\/code><\/pre>\n<p>Your application will reply with a large amount of information in JSON<br \/>\nformat about your user account. To test the other, non-special user<br \/>\nuser2, modify the keycloak-session-create.sh script to authenticate that<br \/>\nuser, create a session token for that user instead, and repeat the HTTP<br \/>\nrequests above for that user. You should get an HTTP 403 error for the<br \/>\nrequest to <a href=\"http:\/\/localhost:8080\/api\/salutations\/user\">http:\/\/localhost:8080\/api\/salutations\/user<\/a>, with the message<br \/>\nbeing <em>\\&quot;The request requires higher privileges than provided by the<br \/>\naccess token.\\&quot;<\/em>, demonstrating that your priviliges restrictions are<br \/>\nworking properly.<\/p>\n<h2>Further Reading<\/h2>\n<p>Spring Boot and Oauth2:<br \/>\n<a href=\"https:\/\/spring.io\/guides\/tutorials\/spring-boot-oauth2\/\">https:\/\/spring.io\/guides\/tutorials\/spring-boot-oauth2\/<\/a><\/p>\n<p>Spring Security and Angular:<br \/>\n<a href=\"https:\/\/spring.io\/guides\/tutorials\/spring-security-and-angular-js\/\">https:\/\/spring.io\/guides\/tutorials\/spring-security-and-angular-js\/<\/a><\/p>\n<p>Baeldung &#8211; Simple SSO with Spring Security Oauth2:<br \/>\n<a href=\"https:\/\/www.baeldung.com\/sso-spring-security-oauth2\">https:\/\/www.baeldung.com\/sso-spring-security-oauth2<\/a><\/p>\n<p>Baeldung &#8211; Spring Oauth2 Angular (Legacy Stack):<br \/>\n<a href=\"https:\/\/www.baeldung.com\/rest-api-spring-oauth2-angular-legacy\">https:\/\/www.baeldung.com\/rest-api-spring-oauth2-angular-legacy<\/a><\/p>\n<p>Keycloak Server: <a href=\"https:\/\/www.keycloak.org\">https:\/\/www.keycloak.org<\/a><\/p>\n<p>Oauth2: <a href=\"https:\/\/oauth.net\/2\/\">https:\/\/oauth.net\/2\/<\/a><\/p>\n<p>Spring Framework: <a href=\"https:\/\/spring.io\/\">https:\/\/spring.io\/<\/a><\/p>\n<p>Java: <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/11\/docs\/api\/index.html\">https:\/\/docs.oracle.com\/en\/java\/javase\/11\/docs\/api\/index.html<\/a><\/p>\n<h2>Acknowledgements<\/h2>\n<p>The author acknowledges the traditional custodians of the Daruk and the<br \/>\nEora People and pays respect to the Elders past and present.<\/p>\n<p>Keycloak\u00ae is a registered trademark of Red Hat and\/or its affiliates.<\/p>\n<p>Oauth is a project of the Internet Engineering Task Force (IETF).<\/p>\n<p>Postman\u00ae is a registered trademark of Postman, Inc.<\/p>\n<p>Docker\u00ae and the Docker logo are trademarks or registered trademarks of<br \/>\nDocker, Inc. in the United States and\/or other countries. Docker, Inc.<br \/>\nand other parties may also have trademark rights in other terms used<br \/>\nherein.<\/p>\n<p>Spring\u00ae and Spring Boot\u00ae are registered trademarks of VMware and\/or its<br \/>\naffiliates.<\/p>\n<p>Linux\u00ae is the registered trademark of Linus Torvalds in the United<br \/>\nStates and other countries.<\/p>\n<p>Oracle\u00ae and Java\u00ae are registered trademarks of Oracle and\/or its<br \/>\naffiliates.<\/p>\n<p>Angular\u00ae is a registered trademark of Alphabet and\/or its affiliates.<\/p>\n<p>Gradle\u00ae is a registered trademark of Gradle, Inc.<\/p>\n<p>Maven\u00ae is a registered trademark of the Apache Foundation.<\/p>\n<p>Other names may be trademarks of their respective owners.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction In this article we will set up a Keycloak Oauth2 Single Sign-On (SSO) server with two clients and two user accounts with differing permissions. We will create a Spring Boot Java application (a \\&quot;resource server\\&quot;) with two REST endpoints, secured by the Keycloak server and requiring different roles for access. We will send HTTP [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":34,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[12,16],"class_list":["post-46","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-java","tag-keycloak","tag-spring"],"_links":{"self":[{"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/posts\/46","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=46"}],"version-history":[{"count":0,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/posts\/46\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=\/wp\/v2\/media\/34"}],"wp:attachment":[{"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=46"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=46"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.cheerfulprogramming.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=46"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}