用于Golang应用程序的Keycloak适配器 [英] Keycloak adaptor for golang application

查看:290
本文介绍了用于Golang应用程序的Keycloak适配器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我将使用keycloak保护我的golang应用程序,但keycloak本身不支持go语言.

I am going to secure my golang application using keycloak, but keycloak itself does not support go language.

在github中有一些go适配器作为一个开放项目,已经将openId connect协议实现为提供程序服务,但是它们没有提供有关如何将库与应用程序集成的示例或文档.

There are some go adaptor as an open project in github that has implemented openId connect protocol as a provider service, but they do not provide an example or documentation on how to integrate libraries with an application.

如何使用golang与keycloak交互?

How can i interact with keycloak using golang?

推荐答案

您已经指出,没有用于golang的官方keycloak适配器. 但是实现它非常简单.这是一个小步.

As you have pointed out, there is no official keycloak adapter for golang. But it is pretty straight forward to implement it. Here is a little walk through.

在此示例中,我将使用官方keycloak docker映像启动服务器. 使用的版本是4.1.0.Final.我认为这也适用于较旧的KeyCloak版本.

For this example, I will use the official keycloak docker image to start the server. The version used is 4.1.0.Final. I think this will work with older KeyCloak versions too though.

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

服务器启动并运行后,可以在浏览器中打开localhost:8080/auth,导航到管理控制台,并使用用户名keycloakk作为相应的密码登录.

After the server is up and running, you can open localhost:8080/auth in your browser, navigate to the administration console and login with username keycloak and k as the corresponding password.

我不会完成创建领域/客户/用户的完整过程.您可以在下面查找 https://www.keycloak.org/docs/latest/server_admin /index.html#admin-console

I will not go through the complete process of creating a realm/clients/users. You can look this up under https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console

这里只是我为重现此示例所做的概述:

Here is just an outline for what I did to reproduce this example:

  1. 创建一个名为demo
  2. 的领域
  3. 为此领域关闭ssl的要求(realmsettings->登录-> require ssl)
  4. 创建一个名为demo-client的客户端(将访问类型"更改为机密)
  5. 使用密码演示创建一个名为demo的用户(用户->添加用户).确保激活并模拟该用户.
  6. 将演示客户端配置为机密,并使用http://localhost:8181/demo/callback作为有效的重定向URI.
  1. create a realm named demo
  2. turn off the requirement of ssl for this realm (realmsettings -> login -> require ssl)
  3. create a client named demo-client (change the "Access Type" to confidential)
  4. create a user named demo with password demo (users -> add user). Make sure to activate and impersonate this user.
  5. configure the demo-client to be confidential and use http://localhost:8181/demo/callback as a valid redirect URI.

生成的keycloak.json(从安装选项卡中获得)如下所示:

The resulting keycloak.json (obtained from the installation tab) looks like this:

{
    "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
}

请注意,您的秘密将有所不同.

Beware that your secret will be different though.

让我们转到go服务器.我将github.com/coreos/go-oidc包用于繁重的工作:

Let's go over to the go server. I use the github.com/coreos/go-oidc package for the heavy lifting:

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

此程序启动具有两个端点的常规http服务器.第一个("/")是您处理的常规端点 应用逻辑.在这种情况下,它只会向您的客户返回"hello world".

This program starts a regular http server with two endpoints. The first one ("/") is your regular endpoint that handles application logic. In this case, it only returns "hello world" to your client.

第二个端点("/demo/callback")用作keycloak的回调.该端点需要在您的计算机上注册 密钥斗篷服务器.成功进行用户身份验证后,Keycloak将发出重定向到该回调URL的链接.重定向包含一些其他查询参数.这些参数包含可用于获取访问/ID令牌的代码.

The second endpoint ("/demo/callback") is used as a callback for keycloak. This endpoint needs to be registered on your keycloak server. Keycloak will issue a redirect to this callback URL upon successful user authentication. The redirect contains some additional query parameters. These parameters contain a code that can be used to obtain access/id tokens.

为了测试此设置,您可以打开一个网络浏览器并将其导航到http://localhost:8181. 该请求应到达您的go服务器,该服务器尝试对您进行身份验证.由于您没有发送令牌,因此转到服务器 会将您重定向到keycloak进行身份验证. 您应该会看到keycloak的登录屏幕.使用您为此领域创建的演示用户(演示/演示)登录. 如果您正确配置了密钥斗篷,它将对您进行身份验证并将您重定向到Go服务器回调.

In order to test this setup you can open a webbrowser and navitage to http://localhost:8181. The request should reach your go server, which tries to authenticate you. Since you did not send a token, the go server will redirecty you to keycloak to authenticate. You should see the login screen of keycloak. Login with the demo user you have created for this realm (demo/demo). If you have configured your keycloak correctly, it will authenticate you and redirect you to your go server callback.

最终结果应该是这样的json

The end result should be a json like this

{
    "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": "demo@demo.com"
    }
}

您可以复制访问令牌并使用curl来验证服务器是否能够接受您的令牌:

You can copy your access token and use curl to verify if the server is able to accept your tokens:

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

您可以在令牌过期后重试-或使用令牌进行尝试.如果这样做,您应该重定向到 再次打开您的密钥库服务器.

You can try it again after the token has expired - or temper with the token. In case you do it, you should get a redirect to your keycloak server again.

这篇关于用于Golang应用程序的Keycloak适配器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆