GO: GIN Microservice mit Keycloak JWT Authentication & Authorization

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.

Keycloak ist somit Ready für die Demo 🙂

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 😉

1 Gedanke zu „GO: GIN Microservice mit Keycloak JWT Authentication & Authorization“

  1. 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!

    Antworten

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.