Browse Source

[CLOUDTRUST-2107] Add a method to retrieve OIDC token

master
sispeo 6 years ago
committed by GitHub
parent
commit
01eb0fdf5d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      Gopkg.lock
  2. 99
      oidc_connect.go
  3. 98
      oidc_connect_test.go

14
Gopkg.lock

@ -3,14 +3,15 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:c3e6e91aafe6e3a12e3669b77f8fd608ddf8e61a727858ce50811daabc9600ea" digest = "1:9cd919baadd8b77a9af99f59b3240f7296f58acd88c7ceb4fe812649be8ae176"
name = "github.com/cloudtrust/common-service" name = "github.com/cloudtrust/common-service"
packages = [ packages = [
".", ".",
"errors", "errors",
"log",
] ]
pruneopts = "" pruneopts = ""
revision = "bda3eb6af01813931780dc33b49aabd0f878be19" revision = "739cff91a05f8dbcf3a0c4e6e57b95089f2a052b"
[[projects]] [[projects]]
digest = "1:379d34d9efc755fab444199f007819fe99718640f9ccfbdd3f0430340bb02b07" digest = "1:379d34d9efc755fab444199f007819fe99718640f9ccfbdd3f0430340bb02b07"
@ -37,17 +38,17 @@
version = "v2.0.0" version = "v2.0.0"
[[projects]] [[projects]]
digest = "1:183b1cb81b770d8033281c5629a4847a2ed7614068bb33c5a9a159d1226b23f0" digest = "1:48e65aaf8ce34ffb3e8d56daa9417826db162afbc2040705db331e9a2e9eebe3"
name = "github.com/go-kit/kit" name = "github.com/go-kit/kit"
packages = [ packages = [
"endpoint", "endpoint",
"log", "log",
"transport", "log/level",
"transport/http", "transport/http",
] ]
pruneopts = "" pruneopts = ""
revision = "150a65a7ec6156b4b640c1fd55f26fd3d475d656" revision = "12210fb6ace19e0496167bb3e667dcd91fa9f69b"
version = "v0.9.0" version = "v0.8.0"
[[projects]] [[projects]]
digest = "1:aa9a6ccd5fd7d29804a27cb0666bc4ac5eb4b73439b7edd54f4b377e5ef8bb47" digest = "1:aa9a6ccd5fd7d29804a27cb0666bc4ac5eb4b73439b7edd54f4b377e5ef8bb47"
@ -242,6 +243,7 @@
input-imports = [ input-imports = [
"github.com/cloudtrust/common-service", "github.com/cloudtrust/common-service",
"github.com/cloudtrust/common-service/errors", "github.com/cloudtrust/common-service/errors",
"github.com/cloudtrust/common-service/log",
"github.com/coreos/go-oidc", "github.com/coreos/go-oidc",
"github.com/gbrlsnchs/jwt", "github.com/gbrlsnchs/jwt",
"github.com/go-kit/kit/transport/http", "github.com/go-kit/kit/transport/http",

99
oidc_connect.go

@ -0,0 +1,99 @@
package keycloak
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
errorhandler "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/common-service/log"
)
// OidcTokenProvider provides OIDC tokens
type OidcTokenProvider interface {
ProvideToken(ctx context.Context) (string, error)
}
type oidcToken struct {
AccessToken string `json:"access_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
RefreshExpiresIn int64 `json:"refresh_expires_in,omitempty"`
TokenType string `json:"token_type,omitempty"`
NotBeforePolicy int `json:"not-before-policy,omitempty"`
SessionState string `json:"session_state,omitempty"`
Scope string `json:"scope,omitempty"`
}
type oidcTokenProvider struct {
timeout time.Duration
tokenURL string
reqBody string
logger log.Logger
oidcToken oidcToken
validUntil int64
}
const (
// Max processing delay: let's assume that the user of OidcTokenProvider will have a maximum of 5 seconds to use the provided OIDC token
maxProcessingDelay = int64(5)
)
// NewOidcTokenProvider creates an OidcTokenProvider
func NewOidcTokenProvider(config Config, realm, username, password, clientID string, logger log.Logger) OidcTokenProvider {
var tokenURL = fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", config.AddrAPI, realm)
// If needed, can add &client_secret={secret}
var body = fmt.Sprintf("grant_type=password&client_id=%s&username=%s&password=%s",
url.QueryEscape(clientID), url.QueryEscape(username), url.QueryEscape(password))
return &oidcTokenProvider{
timeout: config.Timeout,
tokenURL: tokenURL,
reqBody: body,
logger: logger,
}
}
func (o *oidcTokenProvider) ProvideToken(ctx context.Context) (string, error) {
if o.validUntil+maxProcessingDelay > time.Now().Unix() {
return o.oidcToken.AccessToken, nil
}
var mimeType = "application/x-www-form-urlencoded"
var httpClient = http.Client{
Timeout: o.timeout,
}
var resp, err = httpClient.Post(o.tokenURL, mimeType, strings.NewReader(o.reqBody))
if err != nil {
o.logger.Warn(ctx, "msg", err.Error())
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse")
}
if err == nil && resp.StatusCode == http.StatusUnauthorized {
o.logger.Warn(ctx, "msg", "Technical user credentials are invalid")
return "", errorhandler.Error{
Status: http.StatusUnauthorized,
Message: errorhandler.GetEmitter() + ".unauthorized",
}
}
if resp.StatusCode >= 400 || resp.Body == http.NoBody || resp.Body == nil {
o.logger.Warn(ctx, "msg", fmt.Sprintf("Unexpected behavior: unexpected http status (%d) or response has no body", resp.StatusCode))
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse")
}
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
err = json.Unmarshal(buf.Bytes(), &o.oidcToken)
if err != nil {
o.logger.Warn(ctx, "msg", fmt.Sprintf("Can't deserialize token. JSON: %s", buf.String()))
return "", errorhandler.CreateInternalServerError("unexpected.oidcToken")
}
o.validUntil = time.Now().Unix() + o.oidcToken.ExpiresIn
return o.oidcToken.AccessToken, nil
}

98
oidc_connect_test.go

@ -0,0 +1,98 @@
package keycloak
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/cloudtrust/common-service/log"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
type TestResponse struct {
StatusCode int
NoBody bool
ResponseBody string
}
func (t *TestResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(t.StatusCode)
if !t.NoBody {
w.Write([]byte(t.ResponseBody))
}
time.Sleep(20 * time.Millisecond)
}
func TestCreateToken(t *testing.T) {
var oidcToken = oidcToken{
AccessToken: `eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZRTUtNUpBb2NOcG5zeEpEaGRyYVdyWlVCZTBMR2xfanNILUtnb1EwWi1FIn0.eyJqdGkiOiIxYjJkZDY2NS01ZGE1LTRiMzAtODY0MS0wNWQ4ZTk0NTQ2ZWQiLCJleHAiOjE1NzkyMDQ0ODYsIm5iZiI6MCwiaWF0IjoxNTc5MTY4NDg2LCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbInBhc3NmbG93LXJlYWxtIiwibXlfcmVhbG0tcmVhbG0iLCJDbG91ZHRydXN0LXJlYWxtIiwibWFzdGVyLXJlYWxtIiwiYWNjb3VudCJdLCJzdWIiOiI3OTU5MjhjMy03N2Y1LTRmMjQtOTI0NC02NzBkMGJmMDJhMmQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5ZTQ5NDA1MS1kZGQ1LTRhODctYTczZC1hOWU5YjMwYmFlZGEiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InBhc3NmbG93LXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJteV9yZWFsbS1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LXJlYWxtIiwidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiQ2xvdWR0cnVzdC1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUgZ3JvdXBzIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJncm91cHMiOlsiL3RvZV9hZG1pbmlzdHJhdG9yIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.GrPUDdQUID0S38ZSwVqarAuSDXrl5cJju3uq32y6bNGPK9a8jcHBP5FEfMcZ3vieQPtWFeySMycwTEcH6x6lc-9bj1w5veL4yyTA1zk_ERPshfiobk0u94vuljnoz-PW7JvBLOy47Bk5cP9pHPbJMPY0kOFHTpXZHd6KwfcE_X8gizLw4rDhIpK1NEtABQVzUNvP9fDZOm2I1PHJbl0odRE7EFu9Xh5ya8DaUQ2RKUb0E5csnA3DYlFdEhtMV1MAKRzqplDzj8zLQ8f8fflzC9_g4vmnDUEKSBxq1f1qKmzm1-XUuqRYTNWHfOtRR9rXrEzn-6fymFcRHIVGW7kgzg`,
ExpiresIn: 3600,
}
var validJSON, _ = json.Marshal(oidcToken)
r := mux.NewRouter()
r.Handle("/auth/realms/nobody/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, NoBody: true})
r.Handle("/auth/realms/invalid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusUnauthorized})
r.Handle("/auth/realms/bad-json/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: `{"truncated-`})
r.Handle("/auth/realms/valid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: string(validJSON)})
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("No body in HTTP response", func(t *testing.T) {
var p = NewOidcTokenProvider(Config{AddrAPI: ts.URL}, "nobody", "user", "passwd", "clientID", log.NewNopLogger())
var _, err = p.ProvideToken(context.TODO())
assert.NotNil(t, err)
})
t.Run("Invalid credentials", func(t *testing.T) {
var p = NewOidcTokenProvider(Config{AddrAPI: ts.URL}, "invalid", "user", "passwd", "clientID", log.NewNopLogger())
var _, err = p.ProvideToken(context.TODO())
assert.NotNil(t, err)
})
t.Run("Invalid JSON", func(t *testing.T) {
var p = NewOidcTokenProvider(Config{AddrAPI: ts.URL}, "bad-json", "user", "passwd", "clientID", log.NewNopLogger())
var _, err = p.ProvideToken(context.TODO())
assert.NotNil(t, err)
})
t.Run("No HTTP response", func(t *testing.T) {
var p = NewOidcTokenProvider(Config{AddrAPI: ts.URL + "0"}, "bad-json", "user", "passwd", "clientID", log.NewNopLogger())
var _, err = p.ProvideToken(context.TODO())
assert.NotNil(t, err)
})
t.Run("Valid credentials", func(t *testing.T) {
var p = NewOidcTokenProvider(Config{AddrAPI: ts.URL}, "valid", "user", "passwd", "clientID", log.NewNopLogger())
var timeStart = time.Now()
// First call
var token, err = p.ProvideToken(context.TODO())
assert.Nil(t, err)
assert.NotEqual(t, "", token)
var timeAfterFirstCall = time.Now()
// Second call
token, err = p.ProvideToken(context.TODO())
assert.Nil(t, err)
assert.NotEqual(t, "", token)
var timeAfterSecondCall = time.Now()
var withHTTPDuration = int64(20 * time.Millisecond)
var withoutHTTPDuration = int64(5 * time.Millisecond)
var duration1 = timeAfterFirstCall.Sub(timeStart).Nanoseconds()
var duration2 = timeAfterSecondCall.Sub(timeAfterFirstCall).Nanoseconds()
var msg = fmt.Sprintf("Durations: no valid token loaded yet:%d (expected > %d), token not expired:%d (expected < %d)", duration1, withHTTPDuration, duration2, withoutHTTPDuration)
assert.True(t, duration1 > withHTTPDuration, msg)
assert.True(t, duration2 < withoutHTTPDuration, msg)
})
}
Loading…
Cancel
Save