= 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
}}}