Ein kurzer Einstieg um einen GIN Microservice mit Keycloak JWT abzusichern.
Folgende GO Packages werden benötigt:
go get -u github.com/gin-gonic/gin go get github.com/tbaehler/gin-keycloak
Zum Testen gibt es einen Dockercontainer mit Keycloak der sich wie folgt starten lässt:
docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.1 start-dev
Ich verwende gerne die Docker compose Umgebungen die mit verschiedenen Datenbank Backends angeboten werden:
keycloak-containers/docker-compose-examples at main · keycloak/keycloak-containers · GitHub
Setup Keycloak
Das ist nur eine Quick & Dirty Einrichtung von Keycloak und ist keinesfalls als 100% geeignet für eine Produktionsumgebung zu verstehen. Um Keycloak vollständig und sauber einzurichten bitte das entsprechende Handbuch durcharbeiten.
Create Realm
Dropdown „Master“ anklicken und „Create Realm“ auswählen. Realm name ist „ginlab“ für dieses Beispiel. Anschließend „Create“ anklicken.
Create Client
Im Realm Dropdown sicherstellen das unser neuer Realm „ginlab“ ausgewählt ist. Jetzt im Menu auf „Clients“ gehen. Hier mit „Create client“ einen neuen Client mit dem Namen „auth“ anlegen. Den Client brauchen wir später um vom Keycloak einen JWT zu bekommen.
Create Client Role
gin-keycloak unterstützt mehrere Arten der Autorisierung, eine ist Anhand einer Client Rolle. Deshalb legen wir eine Client Rolle in „auth“ an namens „client_role“. Im Menü auf „Clients“ gehen und den Client „auth“ auswählen. Anschließend im Tabmenü „Roles“ anklicken. Jetzt kann eine neue Rolle über „Create role“ erzeugt werden.
Create Realm Role
Die andere Variante ist Rollen im Rahmen des Realm zu verwalten. Hier legen wir für die Demo ebenfalls eine Rolle namens „realm_role“ an. Dazu im Menü auf „Realm roles“ gehen. Hier ebenfalls „Create role“ anklicken.
Create User
Natürlich benötigen wir für die Tests einen User dieser ist im Menü über „Users“ -> „Add user“ einzurichten.
Anschließend müssen wir dem User noch ein Passwort geben. Das geht über das Tabmenü „Creadentials“. Im Dialog „Temporary“ ausschalten, für die Demo verwende ich das Passwort „test“.
Assign Client Role to User
Jetzt müssen noch die entsprechenden Rollen dem User zugewiesen werden. Als erstes weisen wir die Client Rolle dem User zu. Dazu im Tabmenü auf „Role mapping“ gehen und „Assign role“ auswählen. Die Schritte Client Rolle und Realm Rolle müssen einzeln gemacht werden. Es muss der Filter von „Filter by roles“ umgestellt werden auf „Filter by Clients“. Hier ist „auth client_role“ welche wir zuvor angelegt haben auszuwählen und mit „Assign“ zu bestätigen.
Assign Realm Role to User
Zuletzt fügen wir noch die Realm Rolle hinzu. Hierfür nochmal auf „Assign role“ gehen und „realm_role“ auswählen und mit „Assign“ bestätigen. Es sollten anschließend in der Übersicht die „default-roles-ginlab“ die „realm_role“ und „auth client_role“ zu sehen sein.
GIN Microservice mit Keycloak Authentication & Authorization
Es werden hier mehrere einzelne Beispiele gezeigt für die Implementierung.
Authentication only
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tbaehler/gin-keycloak/pkg/ginkeycloak" ) func main() { // ONLY AUTH kconfig := ginkeycloak.KeycloakConfig{ Url: "http://10.1.1.143:8080", Realm: "ginlab", } r := gin.Default() // ONLY AUTHENTICATED EXAMPLE only_auth := r.Group("/api/only_auth") only_auth.Use(ginkeycloak.Auth(ginkeycloak.AuthCheck(), kconfig)) only_auth.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "secret only auth"}) }) // ONLY AUTHENTICATED EXAMPLE only_auth_no_access := r.Group("/api/only_auth_no_access") only_auth_no_access.Use(ginkeycloak.Auth(ginkeycloak.AuthCheck(), kconfig)) only_auth_no_access.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "no access"}) }) r.Run() }
Mit „go run auth_only.go“ kann der GIN Microservice jetzt getestet werden.
Versuche mit CURL
Den Token habe ich aufgrund seiner Länge hier in den Beispielen durch „<sehr langer token>“ ersetzt. Für alle weiteren Beispiele kann der gleiche Aufruf verwendet werden um einen JWT Token zu erhalten. Den „access_token“ muss man mit export TOKEN=<sehr langer token> verfügbar machen um ihn in den weiteren Beispielen zu verwenden. Der Token ist nur 300 Sekunden gültig, diese Einstellung ist aber anpassbar im Keycloak damit man nicht immer wieder im Lab einen neuen erzeugen muss.
JWT Token
# Erzeugen eines JWT Tokens curl -d 'client_id=auth' -d 'username=testuser' -d 'password=test' -d 'grant_type=password' 'http://10.1.1.143:8080/auth/realms/ginlab/protocol/openid-connect/token' | jq # Ausgabe: % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2275 100 2209 100 66 16609 496 --:--:-- --:--:-- --:--:-- 17105 { "access_token": "<sehr langer token>", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "<refresh token>", "token_type": "Bearer", "not-before-policy": 0, "session_state": "46a1655c-8785-484c-b66d-2b61f7919aaa", "scope": "email profile" } # Token als Bash Variable setzen export TOKEN=<sehr langer token>
Versuch 1: Mit JWT Token
# Versuch 1: Erfolgreich mit JWT curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/only_auth/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 30 100 30 0 0 3333 0 --:--:-- --:--:-- --:--:-- 3750 { "message": "secret only auth" } # Ausgabe GIN LOG: {f123e3a8-c4ee-44de-bae9-2114077e40d2 1668534202 0 1668533902 http://10.1.1.143:8080/auth/realms/ginlab b397379d-88a9-4a2b-8da8-f9faef3260c3 Bearer auth 0 3ddb675e-2f6d-41dc-a775-158e9dfe7ad7 1 [] map[account:{[manage-account manage-account-links view-profile]} auth:{[client_role]}] testuser {[realm_role default-roles-ginlab offline_access uma_authorization]}} [GIN] 2022/11/15 - 19:38:38 | 200 | 517.8µs | 127.0.0.1 | GET "/api/only_auth/"
Versuch 2: Ohne Token
curl http://localhost:8080/api/only_auth_no_access/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 # Ausgabe GINH LOG: ERROR: logging before flag.Parse: E1115 18:40:21.150915 17056 gin_keycloak.go:189] [Gin-OAuth] Can not extract oauth2.Token, caused by: No authorization header [GIN] 2022/11/15 - 19:40:21 | 401 | 1.2505ms | 127.0.0.1 | GET "/api/only_auth_no_access/" Error #01: No token in context
Single User Access
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tbaehler/gin-keycloak/pkg/ginkeycloak" ) func main() { // FOR REALM, USER, CLIENT AUTH config := ginkeycloak.BuilderConfig{ Service: "auth", Url: "http://10.1.1.143:8080", Realm: "ginlab", } r := gin.Default() // USER ACCESS EXAMPLE private_user := r.Group("/api/private_user") private_user.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForUid("testuser"). Build()) private_user.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "secret with user access"}) }) // USER ACCESS EXAMPLE NO ACCESS private_user_no_access := r.Group("/api/private_user_no_access") private_user_no_access.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForUid("testuser_no_access"). Build()) private_user_no_access.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "no access"}) }) r.Run() }
Mit „go run single_user.go“ kann der GIN Microservice jetzt getestet werden.
Versuche mit CURL
Es wird ein Token benötigt siehe „JWT Token“.
Versuch 1: Mit JWT Token
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/private_user/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 37 100 37 0 0 536 0 --:--:-- --:--:-- --:--:-- 536 { "message": "secret with user access" } # Ausgabe GIN Log: {3d9cb91d-2f20-409a-b14f-893b563bf18c 1668534962 0 1668534662 http://10.1.1.143:8080/auth/realms/ginlab b397379d-88a9-4a2b-8da8-f9faef3260c3 Bearer auth 0 6471aed3-9c8d-44e8-8088-aebdce5c86dc 1 [] map[account:{[manage-account manage-account-links view-profile]} auth:{[client_role]}] testuser {[realm_role default-roles-ginlab offline_access uma_authorization]}} [GIN] 2022/11/15 - 19:51:27 | 200 | 14.3114ms | 127.0.0.1 | GET "/api/private_user/"
Versuch 2: Zugriff auf Bereich ohne Berechtigung
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/private_user_no_access/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 # Ausgabe GIN Log: [GIN] 2022/11/15 - 19:52:09 | 403 | 0s | 127.0.0.1 | GET "/api/private_user_no_access/" Error #01: Access to the Resource is forbidden
Realm Role Access
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tbaehler/gin-keycloak/pkg/ginkeycloak" ) func main() { // FOR REALM, USER, CLIENT AUTH config := ginkeycloak.BuilderConfig{ Service: "auth", Url: "http://10.1.1.143:8080", Realm: "ginlab", } r := gin.Default() // REALM ROLE EXAMPLE secured_realm := r.Group("/api/realm_role") secured_realm.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForRealm("realm_role"). Build()) secured_realm.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "secret with realm role"}) }) // REALM ROLE EXAMPLE NO ACCESS secured_realm_no_access := r.Group("/api/realm_role_no_access") secured_realm_no_access.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForRealm("realm_role_no_access"). Build()) secured_realm_no_access.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "no access"}) }) r.Run() }
Mit „go run realm_role.go“ kann der GIN Microservice jetzt getestet werden.
Versuche mit CURL
Es wird ein Token benötigt siehe „JWT Token“.
Versuch 1: User hat die Realm Rolle
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/realm_role/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 36 100 36 0 0 500 0 --:--:-- --:--:-- --:--:-- 500 { "message": "secret with realm role" # Ausgabe GIN Log: {b3471853-934f-46da-a0cc-ac908f08b863 1668539093 0 1668538793 http://10.1.1.143:8080/auth/realms/ginlab b397379d-88a9-4a2b-8da8-f9faef3260c3 Bearer auth 0 d7eb5ef4-b604-4a9f-b88d-9c2f62fe1c7b 1 [] map[account:{[manage-account manage-account-links view-profile]} auth:{[client_role]}] testuser {[realm_role default-roles-ginlab offline_access uma_authorization]}} [GIN] 2022/11/15 - 20:01:06 | 200 | 16.0824ms | 127.0.0.1 | GET "/api/realm_role/"
Versuch 2: User hat nicht die Realm Rolle
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/realm_role_no_access/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 # Ausgabe GIN Log: [GIN] 2022/11/15 - 20:01:12 | 403 | 0s | 127.0.0.1 | GET "/api/realm_role_no_access/" Error #01: Access to the Resource is forbidden
Client Role Access
package main import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tbaehler/gin-keycloak/pkg/ginkeycloak" ) func main() { // FOR REALM, USER, CLIENT AUTH config := ginkeycloak.BuilderConfig{ Service: "auth", Url: "http://10.1.1.143:8080", Realm: "ginlab", } r := gin.Default() // CLIENT ROLE EXAMPLE secured_client := r.Group("/api/client_role") secured_client.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForRole("client_role"). Build()) secured_client.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "secret with client role"}) }) // CLIENT ROLE EXAMPLE NO ACCESS secured_client_no_access := r.Group("/api/client_role_no_access") secured_client_no_access.Use(ginkeycloak.NewAccessBuilder(config). RestrictButForRole("client_role_no_access"). Build()) secured_client_no_access.GET("/", func(c *gin.Context) { ginToken, _ := c.Get("token") token := ginToken.(ginkeycloak.KeyCloakToken) fmt.Println(token) c.JSON(http.StatusOK, gin.H{"message": "secret with client role"}) }) r.Run() }
Mit „go run client_role.go“ kann der GIN Microservice jetzt getestet werden.
Versuche mit CURL
Es wird ein Token benötigt siehe „JWT Token“.
Versuch 1: User hat die Client Rolle
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/client_role/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 37 100 37 0 0 406 0 --:--:-- --:--:-- --:--:-- 411 { "message": "secret with client role" } # Ausgabe GIN Log: {b3471853-934f-46da-a0cc-ac908f08b863 1668539093 0 1668538793 http://10.1.1.143:8080/auth/realms/ginlab b397379d-88a9-4a2b-8da8-f9faef3260c3 Bearer auth 0 d7eb5ef4-b604-4a9f-b88d-9c2f62fe1c7b 1 [] map[account:{[manage-account manage-account-links view-profile]} auth:{[client_role]}] testuser {[realm_role default-roles-ginlab offline_access uma_authorization]}} [GIN] 2022/11/15 - 20:02:30 | 200 | 28.9797ms | 127.0.0.1 | GET "/api/client_role/"
Versuch 2: User hat nicht die Client Rolle
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/client_role_no_access/ | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 # Ausgabe GIN Log: [GIN] 2022/11/15 - 20:02:35 | 403 | 0s | 127.0.0.1 | GET "/api/client_role_no_access/" Error #01: Access to the Resource is forbidden
Viel Spaß bei euren Implementierungen 😉
Vielen Dank für die Weitergabe dieser Informationen. Ich habe mir den Gin-Keycloak-Quellcode angesehen. Scheint von Zalando unterstützt zu werden und wurde ständig aktualisiert. Ich wünschte, es hätte eine größere Benutzerbasis. Ich fühle mich im Moment etwas unsicher, es zu benutzen. Wenn ich jedoch den Quellcode lese, kann ich sehen, dass es sich lediglich um einen netten Wrapper über ouath2 für go handelt, einer weitaus größeren und häufiger genutzten Bibliothek. Gin-Cloak scheint das Wesentliche für die Autorisierung von Benutzern bereitzustellen. Ich muss noch viel weiterlesen, bis ich zu dem Schluss komme, dass ich diese Bibliothek nutzen werde. Danke schön!