= keycloak = Open Source Identity and Access Management. * https://www.keycloak.org/ == OIDC == * https://www.scottbrady91.com/OpenID-Connect/OpenID-Connect-Overview OpenID Connect (OIDC) provides a simple identity layer on top of the OAuth 2.0 protocol, enabling Single Sign-On (SSO) and API access in one round trip. It brings the missing user authentication story and identity layer to OAuth. == Steps setup realm == {{{#!highlight bash cd /tmp wget https://github.com/keycloak/keycloak/releases/download/14.0.0/keycloak-14.0.0.zip unzip -t keycloak-14.0.0.zip unzip keycloak-14.0.0.zip cd ~/tmp/keycloak-14.0.0/bin sh standalone.sh http://localhost:8080/auth }}} === Create admin user === * http://localhost:8080/auth * Administration Console * User: admin * Password: admin * Password confirmation: admin * Click on Create === Create realm === * http://localhost:8080/auth/admin/master/console/#/realms/master * login with admin:admin * http://localhost:8080/auth/admin/master/console/#/create/realm * Name: MyRealm * Enabled: On * Click on Create === Add user myuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm * Go to Users * Click on Add user * Username: myuser * User enabled: ON * Save === Add user mysubtaskuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm * Go to Users * Click on Add user * Username: mysubtaskuser * User enabled: ON * Save === Set user password myuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm/users * Select user myuser * Select credentials tab * Password: mypwd * Password confirmation: mypwd * Temporary: off * Click on "Set Password" === Set user password mysubtaskuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm/users * Select user mysubtaskuser * Select credentials tab * Password: mypwd2 * Password confirmation: mypwd2 * Temporary: off * Click on "Set Password" === Create role USER === * http://localhost:8080/auth/admin/master/console/#/create/role/MyRealm * Add role USER to MyRealm * Role name: USER * Click on Save === Create role USERSUBTASK === * http://localhost:8080/auth/admin/master/console/#/create/role/MyRealm * Add role USERSUBTASK to MyRealm * Role name: USERSUBTASK * Click on Save === Associate role to user myuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm/users * select user myuser * select tab Role mappings * select USER role and click on add selected === Associate role to user mysubtaskuser === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm/users * select user mysubtaskuser * select tab Role mappings * select USERSUBTASK role and click on add selected === Create keycloak client === * http://localhost:8080/auth/admin/master/console/#/realms/MyRealm/clients * click on create * client id: curl_confidential * client protocol: openid-connect * root url: http://localhost:8080 * Click on save * Clients Curl_confidential settings: * access-type: confidential * Should appear tab Credentials * Client authenticator: Client ID and secret * Click on "Regenerate Secret" * # 3a862f1b-6687-4f7a-8e04-be494fca99e0 * Clients Curl_confidential Mappers Add builtin "realm roles", "groups" * add selected * For each map add "Add to userinfo" * Clients Curl_confidential Scope, * select full scope allowed: ON === client data === * realm: MyRealm * user pwd: myuser mypwd * client id: curl_confidential * protocol: openid-connect * Curl_confidential settings: * access-type confidential * valid redirect url http://localhost:8080 * tab credentials: regenerate secret 3a862f1b-6687-4f7a-8e04-be494fca99e0 === Signout === * http://localhost:8080/auth/realms/MyRealm/account/ === cUrl calls to test keycloak === {{{#!highlight bash ACCESS_TOKEN=$(curl -d 'client_id=curl_confidential' -d 'client_secret=3a862f1b-6687-4f7a-8e04-be494fca99e0' -d 'username=myuser' -d 'password=mypwd' -d 'grant_type=password' 'http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/token' | json_reformat | jq -r '.access_token') echo $ACCESS_TOKEN curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo | json_reformat curl -X GET -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/.well-known/openid-configuration | json_reformat }}} {{{#!highlight bash CLIENT_ID="curl_confidential" CLIENT_SECRET="3a862f1b-6687-4f7a-8e04-be494fca99e0" TOKEN=$(curl -d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" -d 'username=myuser' -d 'password=mypwd' -d 'grant_type=password' 'http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/token' | json_reformat) ACCESS_TOKEN=$(echo $TOKEN | jq -r '.access_token') REFRESH_TOKEN=$(echo $TOKEN | jq -r '.refresh_token') curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo curl -vvv -d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" -d "refresh_token=$REFRESH_TOKEN" -H "Bearer: $ACCESS_TOKEN" 'http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/logout' curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo # }}} == Setup keycloak as service in Raspberry pi == * /etc/init.d/keycloak {{{#!highlight bash #! /bin/sh ### BEGIN INIT INFO # Provides: keycloak # Default-Start: 2 3 4 5 # Default-Stop: # Short-Description: keycloak # Description: keycloak ### END INIT INFO # # Some things that run always touch /var/lock/keycloak # Carry out specific functions when asked to by the system case "$1" in start) echo "Starting script keycloak " su pi -c "nohup /home/pi/keycloak-14.0.0/bin/standalone.sh &" ;; stop) echo "Stopping script keycloak" kill $(ps uax | grep keycloak | grep java | awk '//{print $2}') ;; status) echo "keycloak PID: $(ps uax | grep keycloak | grep java | awk '//{print $2}')" ;; *) echo "Usage: /etc/init.d/keycloak {start|stop|status}" exit 1 ;; esac exit 0 }}} == Keycloak 21.1.1 + SpringBoot 3.1 + Spring Security + AspectJ (AOP) == {{{#!highlight sh cd ~ wget https://github.com/keycloak/keycloak/releases/download/21.1.1/keycloak-21.1.1.zip unzip keycloak-21.1.1.zip cd keycloak-21.1.1/bin bash kc.sh start bash kc.sh show-config keytool -genkeypair -alias debian -keyalg RSA -keysize 2048 -validity 365 -keystore server.keystore -dname "cn=Server Administrator,o=Keycloak,c=PT" -keypass secret -storepass secret cp server.keystore ../conf ./kc.sh start-dev --hostname=debian --https-key-store-password=secret #Sign in to your account #Master, Create realm, MyRealm , Create #Users, Create new user, myuser, create #select user, credentials, set password, mypwd mypwd, temporary off , save, save password #Realm roles, create role, USER, save #Users, myuser, role mapping, assign role USER #signout #http://debian:8080/admin/master/console/#/MyRealm #My realm, clients, create client # client type: openid connect # client id: curl_confidential # next # client authentication: on # standard flow, direct access grants # next # valid redirect url http://localhost:8080 # save # tab credentials of curl_confidential # client secret regenerate -> Cymorm3jWN2b5z49dNASwPWwgY5zAsdV curl -d 'client_id=curl_confidential' -d 'client_secret=Cymorm3jWN2b5z49dNASwPWwgY5zAsdV' -d 'usr' -d 'password=mypwd' -d 'grant_type=password' 'http://localhost:8080/realms/MyRealm/protocol/openid-connect/token' sudo apt install jq TOKEN=$(curl -d 'client_id=curl_confidential' -d 'client_secret=Cymorm3jWN2b5z49dNASwPWwgY5zAsdV' -d 'username=myuser' -d 'password=mypwd' -d 'grant_type=password' 'http://localhost:8080/realms/MyRealm/protocol/openid-connect/token') echo $TOKEN mkdir -p ~/Documents/test-springboot-keycloak/proj cd ~/Documents/test-springboot-keycloak/proj touch pom.xml touch src/main/java/com/example/demo/UserRole.java touch src/main/java/com/example/demo/RolesAspect.java touch src/main/java/com/example/demo/SecurityConfiguration.java touch src/main/java/com/example/demo/AdminRole.java touch src/main/java/com/example/demo/DemoApplication.java touch src/main/resources/application.properties }}} === pom.xml === {{{#!highlight xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.1.0 com.example demo 0.0.1-SNAPSHOT demo Demo project for Spring Boot 17 org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-maven-plugin }}} === src/main/java/com/example/demo/UserRole.java === {{{#!highlight java package com.example.demo; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.RetentionPolicy; /** * Annotation to identify code associated with USER role * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface UserRole { } }}} === src/main/java/com/example/demo/RolesAspect.java === {{{#!highlight java package com.example.demo; import java.util.List; import java.util.ArrayList; import java.util.Map; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.jwt.Jwt; @Aspect @Component public class RolesAspect { private boolean hasRole(String role, ProceedingJoinPoint joinPoint) { if (joinPoint.getArgs() != null && joinPoint.getArgs().length >= 1) { Object authArg = joinPoint.getArgs()[0]; if (Authentication.class.isAssignableFrom(authArg.getClass())) { Jwt jwt = (Jwt) ((Authentication) authArg).getCredentials(); Map realmAccess = jwt.getClaimAsMap("realm_access"); List roles = (ArrayList) realmAccess.get("roles"); if (roles.contains(role)) { return true; } } } return false; } /** * Intercept stuff annotated with UserRole annotation * * @param joinPoint * @return * @throws Throwable */ @Around("@annotation(UserRole)") public Object interceptUserRoleAnnotation(ProceedingJoinPoint joinPoint) throws Throwable { if (hasRole("USER", joinPoint)) { return joinPoint.proceed(); } else { System.out.println("USER role not found"); return null; } } /** * Intercept stuff annotated with AdminRole annotation * * @param joinPoint * @return * @throws Throwable */ @Around("@annotation(AdminRole)") public Object interceptAdminRoleAnnotation(ProceedingJoinPoint joinPoint) throws Throwable { if (hasRole("ADMIN", joinPoint)) { return joinPoint.proceed(); } else { System.out.println("ADMIN role not found"); return null; } } } }}} === src/main/java/com/example/demo/SecurityConfiguration.java === {{{#!highlight java package com.example.demo; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfiguration { @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.oauth2ResourceServer( (oauth2ResourceServer) -> { oauth2ResourceServer.jwt((jwt) -> { jwt.decoder(null); }); }).build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().requestMatchers("/otherhello**").requestMatchers("/static**"); } } }}} === src/main/java/com/example/demo/AdminRole.java === {{{#!highlight java package com.example.demo; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.RetentionPolicy; /** * Annotation to identify code associated with ADMIN role * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AdminRole { } }}} === src/main/java/com/example/demo/DemoApplication.java === {{{#!highlight java package com.example.demo; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @RestController @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping("/otherhello") public String otherHello() { return "other hello"; } @GetMapping("/hello") @UserRole public String hello(Authentication authentication) { String authorities = ""; for (int authorityIndex = 0; authorityIndex < authentication.getAuthorities().size(); authorityIndex++) { GrantedAuthority ga = (GrantedAuthority) authentication.getAuthorities().toArray()[authorityIndex]; authorities += ga.getAuthority() + " "; } Jwt jwt = (Jwt) authentication.getCredentials(); Object[] keys = jwt.getClaims().keySet().toArray(); String allkeys = ""; String preferredUsername = jwt.getClaim("preferred_username").toString(); Map realmAccess = jwt.getClaimAsMap("realm_access"); List roles = (ArrayList) realmAccess.get("roles"); System.out.println("Contains USER " + roles.contains("USER")); for (int j = 0; j < keys.length; j++) { allkeys = allkeys + " " + (String) keys[j] + ":" + jwt.getClaims().get(keys[j]).toString() + " "; } return "I am authenticated with user " + authentication.getName() + " Authorities: " + authorities + " Details: " + authentication.getDetails().toString() + " allKeys: " + allkeys + " ... " + preferredUsername + " ... " + roles; } @GetMapping("/helloAdmin") @AdminRole public String helloAdmin(Authentication authentication) { return "Hello ADMIN"; } @GetMapping("/helloUser/{text}") @UserRole public String helloUser(Authentication authentication, @PathVariable String text) { return "Hello USER " + text; } } }}} === src/main/resources/application.properties === {{{#!highlight sh server.port=8081 spring.security.oauth2.resourceserver.jwt.issuer-uri=http://debian:8080/realms/MyRealm #logging.level.root=DEBUG logging.level.root=INFO logging.file=/tmp/testout.log }}}