web-dev-qa-db-fra.com

Adaptateur Keycloak pour l'application Golang

Je vais sécuriser mon application golang en utilisant keycloak, mais keycloak lui-même ne prend pas en charge la langue go.

Il existe des adaptateurs go en tant que projet ouvert dans github qui ont implémenté le protocole de connexion openId en tant que service fournisseur, mais ils ne fournissent pas d'exemple ni de documentation sur la façon d'intégrer des bibliothèques à une application.

Comment puis-je interagir avec Keycloak en utilisant Golang?

14
setiabb

Comme vous l'avez souligné, il n'y a pas d'adaptateur officiel pour le golang. Mais il est assez simple de le mettre en œuvre. Voici une petite promenade.

Serveur Keycloak

Pour cet exemple, j'utiliserai l'image docker officielle de keycloak pour démarrer le serveur. La version utilisée est 4.1.0.Final. Je pense que cela fonctionnera aussi avec les anciennes versions de KeyCloak.

docker run -d -p 8080:8080 -e KEYCLOAK_USER=keycloak -e KEYCLOAK_PASSWORD=k --name keycloak jboss/keycloak:4.1.0.Final

Une fois le serveur opérationnel, vous pouvez ouvrir localhost:8080/auth dans votre navigateur, accédez à la console d'administration et connectez-vous avec le nom d'utilisateur keycloak et k comme mot de passe correspondant.

Je ne passerai pas par le processus complet de création d'un domaine/clients/utilisateurs. Vous pouvez le rechercher sous https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console

Voici juste un aperçu de ce que j'ai fait pour reproduire cet exemple:

  1. créer un domaine nommé demo
  2. désactivez l'exigence de ssl pour ce domaine (realmsettings -> login -> require ssl)
  3. créer un client nommé demo-client
  4. créer un utilisateur nommé démo avec démo de mot de passe (utilisateurs -> ajouter un utilisateur). Assurez-vous d'activer et d'usurper l'identité de cet utilisateur.
  5. configurer le client de démonstration pour qu'il soit confidentiel et utiliser http://localhost:8181/demo/callback comme URI de redirection valide.

Le keycloak.json résultant (obtenu à partir de l'onglet d'installation) ressemble à ceci:

{
    "realm": "demo",
    "auth-server-url": "http://localhost:8080/auth",
    "ssl-required": "none",
    "resource": "demo-client",
    "credentials": {
        "secret": "cbfd6e04-a51c-4982-a25b-7aaba4f30c81"
    },
    "confidential-port": 0
}

Attention cependant, votre secret sera différent.

Le serveur Go

Passons au serveur go. Je utilise le github.com/coreos/go-oidc emballage pour le levage de charges lourdes:

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "strings"

    oidc "github.com/coreos/go-oidc"
    "golang.org/x/oauth2"
)

func main() {
    configURL := "http://localhost:8080/auth/realms/demo"
    ctx := context.Background()
    provider, err := oidc.NewProvider(ctx, configURL)
    if err != nil {
        panic(err)
    }

    clientID := "demo-client"
    clientSecret := "cbfd6e04-a51c-4982-a25b-7aaba4f30c81"

    redirectURL := "http://localhost:8181/demo/callback"
    // Configure an OpenID Connect aware OAuth2 client.
    oauth2Config := oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        // Discovery returns the OAuth2 endpoints.
        Endpoint: provider.Endpoint(),
        // "openid" is a required scope for OpenID Connect flows.
        Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
    }
    state := "somestate"

    oidcConfig := &oidc.Config{
        ClientID: clientID,
    }
    verifier := provider.Verifier(oidcConfig)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        rawAccessToken := r.Header.Get("Authorization")
        if rawAccessToken == "" {
            http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
            return
        }

        parts := strings.Split(rawAccessToken, " ")
        if len(parts) != 2 {
            w.WriteHeader(400)
            return
        }
        _, err := verifier.Verify(ctx, parts[1])

        if err != nil {
            http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
            return
        }

        w.Write([]byte("hello world"))
    })

    http.HandleFunc("/demo/callback", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Query().Get("state") != state {
            http.Error(w, "state did not match", http.StatusBadRequest)
            return
        }

        oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
        if err != nil {
            http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
            return
        }
        rawIDToken, ok := oauth2Token.Extra("id_token").(string)
        if !ok {
            http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
            return
        }
        idToken, err := verifier.Verify(ctx, rawIDToken)
        if err != nil {
            http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
            return
        }

        resp := struct {
            OAuth2Token   *oauth2.Token
            IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
        }{oauth2Token, new(json.RawMessage)}

        if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        data, err := json.MarshalIndent(resp, "", "    ")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        w.Write(data)
    })

    log.Fatal(http.ListenAndServe("localhost:8181", nil))
}

Ce programme démarre un serveur http standard avec deux points de terminaison. Le premier ("/") est votre point de terminaison standard qui gère la logique d'application. Dans ce cas, il ne renvoie que "bonjour le monde" à votre client.

Le deuxième point de terminaison ("/ demo/callback") est utilisé comme rappel pour keycloak. Ce point de terminaison doit être enregistré sur votre serveur keycloak. Keycloak émettra une redirection vers cette URL de rappel en cas d'authentification réussie de l'utilisateur. La redirection contient des paramètres de requête supplémentaires. Ces paramètres contiennent un code qui peut être utilisé pour obtenir des jetons d'accès/id.

Vérifiez votre configuration

Afin de tester cette configuration, vous pouvez ouvrir un navigateur Web et navitage pour http://localhost:8181. La demande devrait atteindre votre serveur go, qui essaie de vous authentifier. Puisque vous n'avez pas envoyé de jeton, le serveur go vous redirigera vers keycloak pour vous authentifier. Vous devriez voir l'écran de connexion de keycloak. Connectez-vous avec l'utilisateur de démonstration que vous avez créé pour ce domaine (démo/démo). Si vous avez correctement configuré votre keycloak, il vous authentifiera et vous redirigera vers votre rappel de serveur go.

Le résultat final devrait être un json comme celui-ci

{
    "OAuth2Token": {
        "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsc1hHR2VxSmx3UUZweWVYR0x6b2plZXBYSEhXUngtTHVJTVVLdDBmNmlnIn0.eyJqdGkiOiI5ZjAxNjM2OC1lYmEwLTRiZjMtYTU5Ni1kOGU1MzdmNTNlZGYiLCJleHAiOjE1MzIxNzM2NTIsIm5iZiI6MCwiaWF0IjoxNTMyMTczMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImRlbW8tY2xpZW50Iiwic3ViIjoiMzgzMzhjOGItYWQ3Zi00NjlmLTgzOTgtMTc5ODk1ODFiYTEyIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZGVtby1jbGllbnQiLCJhdXRoX3RpbWUiOjE1MzIxNzMzNTIsInNlc3Npb25fc3RhdGUiOiJjZTg2NWFkZC02N2I4LTQ5MDUtOGYwMy05YzE2MDNjMWJhMGQiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImRlbW8iLCJlbWFpbCI6ImRlbW9AZGVtby5jb20ifQ.KERz8rBddxM9Qho3kgigX-fClWqbKY-3JcWT3JOQDoLa-prkorfa40BWlyf9ULVgjzT2d8FLJpqQIQYvucKU7Q7vFBVIjTGucUZaE7b6JGMea5H34A1i-MNm7L2CzDJ2GnBONhNwLKoftTSl0prbzwkzcVrps-JAZ6L2gssSa5hBBGJYBKAUfm1OIb57Jq0vzro3vLghZ4Ay7iNunwfcHUrxiFJfUjaU6PQwzrA5pnItOPuavJFUgso7-3JLtn3X9GQuyyZKrkDo6-gzU0JZmkQQzAXXgt43NxooryImuacwSB5xbIKY6qFkedldoOPehld1-oLv0Yy_FIwEad3uLw",
        "token_type": "bearer",
        "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsc1hHR2VxSmx3UUZweWVYR0x6b2plZXBYSEhXUngtTHVJTVVLdDBmNmlnIn0.eyJqdGkiOiI0MjdmMTlhYy1jMTkzLTQ2YmQtYWFhNi0wY2Q1OTI5NmEwMGQiLCJleHAiOjE1MzIxNzUxNTIsIm5iZiI6MCwiaWF0IjoxNTMyMTczMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImRlbW8tY2xpZW50Iiwic3ViIjoiMzgzMzhjOGItYWQ3Zi00NjlmLTgzOTgtMTc5ODk1ODFiYTEyIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImRlbW8tY2xpZW50IiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiY2U4NjVhZGQtNjdiOC00OTA1LThmMDMtOWMxNjAzYzFiYTBkIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIn0.FvvDW6ZSH8mlRR2zgaN1zesX14SmkCs9RrIVU4Jn1-SHVdKEA6YKur0-RUAFTObQDMLVhFFJ05AjGVGWpBrgVDcAwW2pI9saM-OHlyTJ3VfFoylgfzakVOIpbIDnHO12UaJrkOI9NWPAJdbBOzBHfsDhKbxhjg4ZX8SwlKr42rV4WWuSRcNu4_YDVO19SiXSCKXVldZ1_2S-qPvViq7VZfaoRLHuYyDvma_ByMsmib9JUkevJ8dxsYxVQ5FWaAfFanh1a1f8HxNRI-Cl180oPn1_Tqq_SYwxzBCw7Q_ENkMirwRS1a4cX9yMVEDW2uvKz2D-OiNAUK8d_ONuPEkTGQ",
        "expiry": "2018-07-21T13:47:28.986686385+02:00"
    },
    "IDTokenClaims": {
        "jti": "f4d56526-37d9-4d32-b99d-81090e92d3a7",
        "exp": 1532173652,
        "nbf": 0,
        "iat": 1532173352,
        "iss": "http://localhost:8080/auth/realms/demo",
        "aud": "demo-client",
        "sub": "38338c8b-ad7f-469f-8398-17989581ba12",
        "typ": "ID",
        "azp": "demo-client",
        "auth_time": 1532173352,
        "session_state": "ce865add-67b8-4905-8f03-9c1603c1ba0d",
        "acr": "1",
        "email_verified": true,
        "preferred_username": "demo",
        "email": "[email protected]"
    }
}

Vous pouvez copier votre jeton d'accès et utiliser curl pour vérifier si le serveur est en mesure d'accepter vos jetons:

# use your complete access token here
export TOKEN="eyJhbG..."
curl -H "Authorization: Bearer $TOKEN" localhost:8181
# output hello world

Vous pouvez réessayer après l'expiration du jeton - ou tempérer avec le jeton. Dans le cas où vous le faites, vous devriez à nouveau obtenir une redirection vers votre serveur keycloak.

21
ShrimpPhaser

Il y a aussi la bibliothèque gocloak qui fournit beaucoup de fonctionnalités. La lib est en développement actif et déjà utilisée dans des projets du monde réel. Ainsi, les éventuels bugs et demandes de fonctionnalités sont traités.

Il fournit des fonctionnalités d'administration telles que "CreateUser", "CreateGroup", etc. et fournit également des fonctions de connexion, de validation de jeton, etc.

Par exemple, créer un utilisateur est aussi simple que:

client := gocloak.NewClient("https://mycool.keycloak.instance")
token, err := client.LoginAdmin("user", "password", "realmName")
if err != nil {
    panic("Something wrong with the credentials or url")
}
user := gocloak.User{
    FirstName: "Bob",
    LastName:  "Uncle",
    EMail:     "[email protected]",
    Enabled:   true,
    Username:  "CoolGuy",
}
client.CreateUser(token.AccessToken, "realm", user)
if err != nil {
    panic("Oh no!, failed to create user :(")
}

Il prend également en charge Introspecting a Requesting Party Token

client := gocloak.NewClient(hostname)
token, err := client.LoginClient(clientid, clientSecret, realm)
if err != nil {
    panic("Login failed:"+ err.Error())
}

rptResult, err := client.RetrospectToken(token.AccessToken, clientid, clientSecret, realm)
if err != nil {
    panic("Inspection failed:"+ err.Error())
}

if !rptResult.Active {
    panic("Token is not active")
}

permissions := rptResult.Permissions
//Do something with the permissions ;) 

Aussi pour gérer l'authentification facile et le rafraîchissement des jetons lors de l'utilisation d'écho, il existe une autre bibliothèque basée sur gocloak appelée gocloak-echo . Cette bibliothèque fournit un gestionnaire et un middleware pour vous aider, mais est toujours dans un état plus WIP.

La bibliothèque fournit également le décodage des accessTokens en revendications personnalisées

Divulgation: je suis l'auteur (principal) de gocloak, donc c'est aussi un peu de publicité, mais en général ça répond à la question. J'ai eu le même problème que l'auteur et j'ai décidé de créer ma propre bibliothèque (basée sur la bibliothèque de quelqu'un d'autre, comme indiqué dans le fichier Lisez-moi sur github).

6
Tobias Theel