keycloak
Open Source Identity and Access Management.
OIDC
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
Create admin user
- 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
cUrl calls to test keycloak
1 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')
2 echo $ACCESS_TOKEN
3
4 curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo | json_reformat
5
6 curl -X GET -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/.well-known/openid-configuration | json_reformat
1 CLIENT_ID="curl_confidential"
2 CLIENT_SECRET="3a862f1b-6687-4f7a-8e04-be494fca99e0"
3 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)
4
5 ACCESS_TOKEN=$(echo $TOKEN | jq -r '.access_token')
6 REFRESH_TOKEN=$(echo $TOKEN | jq -r '.refresh_token')
7
8 curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo
9
10 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'
11
12 curl -X POST -d "access_token=$ACCESS_TOKEN" http://localhost:8080/auth/realms/MyRealm/protocol/openid-connect/userinfo
13 #
14
Setup keycloak as service in Raspberry pi
- /etc/init.d/keycloak
1 #! /bin/sh
2 ### BEGIN INIT INFO
3 # Provides: keycloak
4 # Default-Start: 2 3 4 5
5 # Default-Stop:
6 # Short-Description: keycloak
7 # Description: keycloak
8 ### END INIT INFO
9 #
10 # Some things that run always
11 touch /var/lock/keycloak
12 # Carry out specific functions when asked to by the system
13 case "$1" in
14 start)
15 echo "Starting script keycloak "
16 su pi -c "nohup /home/pi/keycloak-14.0.0/bin/standalone.sh &"
17 ;;
18 stop)
19 echo "Stopping script keycloak"
20 kill $(ps uax | grep keycloak | grep java | awk '//{print $2}')
21 ;;
22 status)
23 echo "keycloak PID: $(ps uax | grep keycloak | grep java | awk '//{print $2}')"
24 ;;
25 *)
26 echo "Usage: /etc/init.d/keycloak {start|stop|status}"
27 exit 1
28 ;;
29 esac
30
31 exit 0
Keycloak 21.1.1 + SpringBoot 3.1 + Spring Security + AspectJ (AOP)
1 cd ~
2 wget https://github.com/keycloak/keycloak/releases/download/21.1.1/keycloak-21.1.1.zip
3 unzip keycloak-21.1.1.zip
4 cd keycloak-21.1.1/bin
5 bash kc.sh start
6 bash kc.sh show-config
7 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
8 cp server.keystore ../conf
9 ./kc.sh start-dev --hostname=debian --https-key-store-password=secret
10 #Sign in to your account
11 #Master, Create realm, MyRealm , Create
12 #Users, Create new user, myuser, create
13 #select user, credentials, set password, mypwd mypwd, temporary off , save, save password
14 #Realm roles, create role, USER, save
15 #Users, myuser, role mapping, assign role USER
16 #signout
17 #http://debian:8080/admin/master/console/#/MyRealm
18 #My realm, clients, create client
19 # client type: openid connect
20 # client id: curl_confidential
21 # next
22 # client authentication: on
23 # standard flow, direct access grants
24 # next
25 # valid redirect url http://localhost:8080
26 # save
27 # tab credentials of curl_confidential
28 # client secret regenerate -> Cymorm3jWN2b5z49dNASwPWwgY5zAsdV
29
30 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'
31 sudo apt install jq
32
33 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')
34 echo $TOKEN
35
36 mkdir -p ~/Documents/test-springboot-keycloak/proj
37 cd ~/Documents/test-springboot-keycloak/proj
38 touch pom.xml
39 touch src/main/java/com/example/demo/UserRole.java
40 touch src/main/java/com/example/demo/RolesAspect.java
41 touch src/main/java/com/example/demo/SecurityConfiguration.java
42 touch src/main/java/com/example/demo/AdminRole.java
43 touch src/main/java/com/example/demo/DemoApplication.java
44 touch src/main/resources/application.properties
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>3.1.0</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.example</groupId>
12 <artifactId>demo</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>demo</name>
15 <description>Demo project for Spring Boot</description>
16 <properties>
17 <java.version>17</java.version>
18 </properties>
19 <dependencies>
20 <dependency>
21 <groupId>org.springframework.boot</groupId>
22 <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
23 </dependency>
24 <dependency>
25 <groupId>org.springframework.boot</groupId>
26 <artifactId>spring-boot-starter-security</artifactId>
27 </dependency>
28 <dependency>
29 <groupId>org.springframework.boot</groupId>
30 <artifactId>spring-boot-starter-web</artifactId>
31 </dependency>
32 <dependency>
33 <groupId>org.springframework.boot</groupId>
34 <artifactId>spring-boot-starter-aop</artifactId>
35 </dependency>
36 </dependencies>
37 <build>
38 <plugins>
39 <plugin>
40 <groupId>org.springframework.boot</groupId>
41 <artifactId>spring-boot-maven-plugin</artifactId>
42 </plugin>
43 </plugins>
44 </build>
45 </project>
src/main/java/com/example/demo/UserRole.java
1 package com.example.demo;
2
3 import java.lang.annotation.Retention;
4 import java.lang.annotation.Target;
5 import java.lang.annotation.ElementType;
6 import java.lang.annotation.RetentionPolicy;
7
8 /**
9 * Annotation to identify code associated with USER role
10 *
11 */
12 @Retention(RetentionPolicy.RUNTIME)
13 @Target(ElementType.METHOD)
14 public @interface UserRole {
15 }
src/main/java/com/example/demo/RolesAspect.java
1 package com.example.demo;
2
3 import java.util.List;
4 import java.util.ArrayList;
5 import java.util.Map;
6
7 import org.aspectj.lang.ProceedingJoinPoint;
8 import org.aspectj.lang.annotation.Around;
9 import org.aspectj.lang.annotation.Aspect;
10 import org.springframework.stereotype.Component;
11 import org.springframework.security.core.Authentication;
12 import org.springframework.security.oauth2.jwt.Jwt;
13
14 @Aspect
15 @Component
16 public class RolesAspect {
17
18 private boolean hasRole(String role, ProceedingJoinPoint joinPoint) {
19 if (joinPoint.getArgs() != null && joinPoint.getArgs().length >= 1) {
20 Object authArg = joinPoint.getArgs()[0];
21 if (Authentication.class.isAssignableFrom(authArg.getClass())) {
22 Jwt jwt = (Jwt) ((Authentication) authArg).getCredentials();
23 Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
24 List<String> roles = (ArrayList<String>) realmAccess.get("roles");
25 if (roles.contains(role)) {
26 return true;
27 }
28 }
29 }
30
31 return false;
32 }
33
34 /**
35 * Intercept stuff annotated with UserRole annotation
36 *
37 * @param joinPoint
38 * @return
39 * @throws Throwable
40 */
41 @Around("@annotation(UserRole)")
42 public Object interceptUserRoleAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
43
44 if (hasRole("USER", joinPoint)) {
45 return joinPoint.proceed();
46 } else {
47 System.out.println("USER role not found");
48 return null;
49 }
50 }
51
52 /**
53 * Intercept stuff annotated with AdminRole annotation
54 *
55 * @param joinPoint
56 * @return
57 * @throws Throwable
58 */
59 @Around("@annotation(AdminRole)")
60 public Object interceptAdminRoleAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
61
62 if (hasRole("ADMIN", joinPoint)) {
63 return joinPoint.proceed();
64 } else {
65 System.out.println("ADMIN role not found");
66 return null;
67 }
68 }
69 }
src/main/java/com/example/demo/SecurityConfiguration.java
1 package com.example.demo;
2
3 import org.springframework.context.annotation.Bean;
4 import org.springframework.context.annotation.Configuration;
5 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6 import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
7 import org.springframework.security.web.SecurityFilterChain;
8
9 @Configuration
10 public class SecurityConfiguration {
11
12 @Bean
13 protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
14 return http.oauth2ResourceServer(
15 (oauth2ResourceServer) -> {
16 oauth2ResourceServer.jwt((jwt) -> {
17 jwt.decoder(null);
18 });
19 }).build();
20 }
21
22 @Bean
23 public WebSecurityCustomizer webSecurityCustomizer() {
24 return (web) -> web.ignoring().requestMatchers("/otherhello**").requestMatchers("/static**");
25 }
26 }
src/main/java/com/example/demo/AdminRole.java
1 package com.example.demo;
2
3 import java.lang.annotation.Retention;
4 import java.lang.annotation.Target;
5 import java.lang.annotation.ElementType;
6 import java.lang.annotation.RetentionPolicy;
7
8 /**
9 * Annotation to identify code associated with ADMIN role
10 *
11 */
12 @Retention(RetentionPolicy.RUNTIME)
13 @Target(ElementType.METHOD)
14 public @interface AdminRole {
15 }
src/main/java/com/example/demo/DemoApplication.java
1 package com.example.demo;
2
3 import java.util.ArrayList;
4 import java.util.List;
5 import java.util.Map;
6
7 import org.springframework.boot.SpringApplication;
8 import org.springframework.boot.autoconfigure.SpringBootApplication;
9 import org.springframework.web.bind.annotation.GetMapping;
10 import org.springframework.web.bind.annotation.PathVariable;
11 import org.springframework.web.bind.annotation.RestController;
12 import org.springframework.security.core.Authentication;
13 import org.springframework.security.core.GrantedAuthority;
14 import org.springframework.security.oauth2.jwt.Jwt;
15
16 @RestController
17 @SpringBootApplication
18 public class DemoApplication {
19
20 public static void main(String[] args) {
21 SpringApplication.run(DemoApplication.class, args);
22 }
23
24 @GetMapping("/otherhello")
25 public String otherHello() {
26 return "other hello";
27 }
28
29 @GetMapping("/hello")
30 @UserRole
31 public String hello(Authentication authentication) {
32 String authorities = "";
33 for (int authorityIndex = 0; authorityIndex < authentication.getAuthorities().size(); authorityIndex++) {
34 GrantedAuthority ga = (GrantedAuthority) authentication.getAuthorities().toArray()[authorityIndex];
35 authorities += ga.getAuthority() + " ";
36 }
37
38 Jwt jwt = (Jwt) authentication.getCredentials();
39
40 Object[] keys = jwt.getClaims().keySet().toArray();
41 String allkeys = "";
42
43 String preferredUsername = jwt.getClaim("preferred_username").toString();
44 Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
45 List<String> roles = (ArrayList<String>) realmAccess.get("roles");
46 System.out.println("Contains USER " + roles.contains("USER"));
47
48 for (int j = 0; j < keys.length; j++) {
49 allkeys = allkeys + " " + (String) keys[j] + ":" + jwt.getClaims().get(keys[j]).toString() + " ";
50 }
51
52 return "I am authenticated with user " + authentication.getName() + " Authorities: " + authorities + " Details: "
53 + authentication.getDetails().toString() + " allKeys: " + allkeys + " ... " + preferredUsername + " ... "
54 + roles;
55 }
56
57 @GetMapping("/helloAdmin")
58 @AdminRole
59 public String helloAdmin(Authentication authentication) {
60 return "Hello ADMIN";
61 }
62
63 @GetMapping("/helloUser/{text}")
64 @UserRole
65 public String helloUser(Authentication authentication, @PathVariable String text) {
66 return "Hello USER " + text;
67 }
68 }