Skip to content

Commit

Permalink
[CLOUDTRUST-5142] Retrieve tokens from a client ID/secret
Browse files Browse the repository at this point in the history
  • Loading branch information
fperot74 authored May 6, 2024
1 parent 8ccbd6b commit 0ced760
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 62 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/protobuf v1.34.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -473,6 +475,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -673,6 +679,10 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
85 changes: 85 additions & 0 deletions toolbox/oidc_client_connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package toolbox

import (
"context"
"fmt"
"strings"

errorhandler "github.com/cloudtrust/common-service/v2/errors"
"github.com/cloudtrust/keycloak-client/v2"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)

// OAuth2Config struct
type OAuth2Config struct {
Realm *string `mapstructure:"realm"`
Username *string `mapstructure:"username"`
Password *string `mapstructure:"password"`
ClientID *string `mapstructure:"client-id"`
ClientSecret *string `mapstructure:"client-secret"`
}

// IsClientConfig checks if the config is a client config or a username/password one
func (oac *OAuth2Config) IsClientConfig() bool {
return oac != nil && oac.Realm != nil && oac.ClientID != nil && oac.ClientSecret != nil
}

type oauth2TokenProvider struct {
perRealmTokenInfo map[string]*oauth2TokenInfo
defaultKey string
logger Logger
}

type oauth2TokenInfo struct {
tokenSource oauth2.TokenSource
token *oauth2.Token
}

// NewOAuth2TokenProvider creates an OidcTokenProvider
func NewOAuth2TokenProvider(kcConfig keycloak.Config, oauth2Config OAuth2Config, logger Logger) OidcTokenProvider {
if !oauth2Config.IsClientConfig() {
return NewOidcTokenProvider(kcConfig, *oauth2Config.Realm, *oauth2Config.Username, *oauth2Config.Password, *oauth2Config.ClientID, logger)
}
var perRealmTokenInfo = make(map[string]*oauth2TokenInfo)
_ = ImportLegacyAddrTokenProvider(&kcConfig)
kcConfig.URIProvider.ForEachTokenURI(func(targetRealm, tokenURI string) {
var cfg = clientcredentials.Config{
ClientID: *oauth2Config.ClientID,
ClientSecret: *oauth2Config.ClientSecret,
TokenURL: fmt.Sprintf(tokenURI, *oauth2Config.Realm),
}
perRealmTokenInfo[targetRealm] = &oauth2TokenInfo{
tokenSource: cfg.TokenSource(context.Background()),
}
})

return &oauth2TokenProvider{
perRealmTokenInfo: perRealmTokenInfo,
defaultKey: kcConfig.URIProvider.GetDefaultKey(),
logger: logger,
}
}

func (o *oauth2TokenProvider) ProvideToken(ctx context.Context) (string, error) {
return o.ProvideTokenForRealm(ctx, o.defaultKey)
}

func (o *oauth2TokenProvider) ProvideTokenForRealm(ctx context.Context, realm string) (string, error) {
var oti *oauth2TokenInfo
var ok bool
if oti, ok = o.perRealmTokenInfo[strings.ToLower(realm)]; !ok {
if realm == o.defaultKey {
return "", errorhandler.CreateInternalServerError("unknownRealm")
}
return o.ProvideTokenForRealm(ctx, o.defaultKey)
}
if oti.token == nil || !oti.token.Valid() {
var err error
oti.token, err = oti.tokenSource.Token()
if err != nil {
return "", err
}
}
return oti.token.AccessToken, nil
}
2 changes: 1 addition & 1 deletion toolbox/oidc_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (o *oidcTokenProvider) ProvideTokenForRealm(ctx context.Context, realm stri
o.logger.Warn(ctx, "msg", err.Error())
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse")
}
if err == nil && resp.StatusCode == http.StatusUnauthorized {
if resp.StatusCode == http.StatusUnauthorized {
o.logger.Warn(ctx, "msg", "Technical user credentials are invalid")
return "", errorhandler.Error{
Status: http.StatusUnauthorized,
Expand Down
147 changes: 93 additions & 54 deletions toolbox/oidc_connect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,31 @@ type TestResponse struct {
}

func (t *TestResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(t.StatusCode)
if !t.NoBody {
w.Write([]byte(t.ResponseBody))
}
time.Sleep(20 * time.Millisecond)
}

func createTechnicalUser(realm string, user string, password string, clientID string) OAuth2Config {
return OAuth2Config{
Realm: &realm,
Username: &user,
Password: &password,
ClientID: &clientID,
}
}

func createServiceAccount(realm string, clientID string, clientSecret string) OAuth2Config {
return OAuth2Config{
Realm: &realm,
ClientID: &clientID,
ClientSecret: &clientSecret,
}
}

func TestCreateToken(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()
Expand Down Expand Up @@ -64,79 +82,100 @@ func TestCreateToken(t *testing.T) {
var invalidURIProvider, _ = NewKeycloakURIProviderFromArray([]string{ts.URL + "0"})
var ctx = context.TODO()

var runFailingTest = func(t *testing.T, uriProvider keycloak.KeycloakURIProvider, realm string) {
t.Run("Technical user", func(t *testing.T) {
var creds = createTechnicalUser(realm, "user", "passwd", "clientID")
var p = NewOAuth2TokenProvider(keycloak.Config{URIProvider: uriProvider}, creds, mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
})
t.Run("Service account", func(t *testing.T) {
var creds = createServiceAccount(realm, "clientID", "client-secret")
var p = NewOAuth2TokenProvider(keycloak.Config{URIProvider: uriProvider}, creds, mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
})
}

t.Run("No body in HTTP response", func(t *testing.T) {
var p = NewOidcTokenProvider(keycloak.Config{URIProvider: uriProvider}, "nobody", "user", "passwd", "clientID", mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
runFailingTest(t, uriProvider, "nobody")
})

t.Run("Invalid credentials", func(t *testing.T) {
var p = NewOidcTokenProvider(keycloak.Config{URIProvider: uriProvider}, "invalid", "user", "passwd", "clientID", mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
runFailingTest(t, uriProvider, "invalid")
})

t.Run("Invalid JSON", func(t *testing.T) {
var p = NewOidcTokenProvider(keycloak.Config{URIProvider: uriProvider}, "bad-json", "user", "passwd", "clientID", mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
runFailingTest(t, uriProvider, "bad-json")
})

t.Run("No HTTP response", func(t *testing.T) {
var p = NewOidcTokenProvider(keycloak.Config{URIProvider: invalidURIProvider}, "bad-json", "user", "passwd", "clientID", mockLogger)
var _, err = p.ProvideToken(ctx)
assert.NotNil(t, err)
runFailingTest(t, invalidURIProvider, "bad-json")
})

t.Run("Valid credentials", func(t *testing.T) {
var p = NewOidcTokenProvider(keycloak.Config{URIProvider: uriProvider}, "valid", "user", "passwd", "clientID", mockLogger)
var runValidCredentialsTest = func(t *testing.T, uriProvider keycloak.KeycloakURIProvider, creds OAuth2Config, logger Logger) {
var p = NewOAuth2TokenProvider(keycloak.Config{URIProvider: uriProvider}, creds, logger)

var timeStart = time.Now()

var timeStart = time.Now()
// First call
var token, err = p.ProvideToken(ctx)
assert.Nil(t, err)
assert.NotEqual(t, "", token)

// First call
var token, err = p.ProvideToken(ctx)
assert.Nil(t, err)
assert.NotEqual(t, "", token)
var timeAfterFirstCall = time.Now()

var timeAfterFirstCall = time.Now()
// Second call
token, err = p.ProvideToken(ctx)
assert.Nil(t, err)
assert.NotEqual(t, "", token)

// Second call
token, err = p.ProvideToken(ctx)
assert.Nil(t, err)
assert.NotEqual(t, "", token)
var timeAfterSecondCall = time.Now()

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

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)
t.Run("Technical user", func(t *testing.T) {
runValidCredentialsTest(t, uriProvider, createTechnicalUser("valid", "user", "passwd", "clientID"), mockLogger)
})
t.Run("Service account", func(t *testing.T) {
runValidCredentialsTest(t, uriProvider, createServiceAccount("valid", "clientID", "client-secret"), mockLogger)
})
})

t.Run("Multiple issuers", func(t *testing.T) {
var anotherIssuer = "second"
var targets = map[string]string{"*": ts.URL, anotherIssuer: ts.URL + "/second"}
var kup, _ = NewKeycloakURIProvider(targets, "*")
var cfg = keycloak.Config{
URIProvider: kup,
Timeout: time.Second,
var runMultipleIssuersTest = func(t *testing.T, creds OAuth2Config) {
var anotherIssuer = "second"
var targets = map[string]string{"*": ts.URL, anotherIssuer: ts.URL + "/second"}
var kup, _ = NewKeycloakURIProvider(targets, "*")
var cfg = keycloak.Config{
URIProvider: kup,
Timeout: time.Second,
}
var p = NewOAuth2TokenProvider(cfg, creds, mockLogger)

var token1, err = p.ProvideTokenForRealm(ctx, "any")
assert.Nil(t, err)
assert.NotEqual(t, "", token1)

var token2 string
token2, err = p.ProvideTokenForRealm(ctx, "another")
assert.Nil(t, err)
assert.Equal(t, token1, token2)

token2, err = p.ProvideTokenForRealm(ctx, anotherIssuer)
assert.Nil(t, err)
assert.NotEqual(t, token1, token2)
}
var p = NewOidcTokenProvider(cfg, "valid", "user", "passwd", "clientID", mockLogger)

var token1, err = p.ProvideTokenForRealm(ctx, "any")
assert.Nil(t, err)
assert.NotEqual(t, "", token1)

var token2 string
token2, err = p.ProvideTokenForRealm(ctx, "another")
assert.Nil(t, err)
assert.Equal(t, token1, token2)

token2, err = p.ProvideTokenForRealm(ctx, anotherIssuer)
assert.Nil(t, err)
assert.NotEqual(t, token1, token2)
t.Run("Technical user", func(t *testing.T) {
runMultipleIssuersTest(t, createTechnicalUser("valid", "user", "passwd", "clientID"))
})
t.Run("Service account", func(t *testing.T) {
runMultipleIssuersTest(t, createServiceAccount("valid", "clientID", "client-secret"))
})
})
}
12 changes: 8 additions & 4 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ github.com/spf13/pflag
# github.com/stretchr/testify v1.8.4
## explicit; go 1.20
github.com/stretchr/testify/assert
# golang.org/x/crypto v0.15.0
# golang.org/x/crypto v0.22.0
## explicit; go 1.18
golang.org/x/crypto/ed25519
golang.org/x/crypto/pbkdf2
# golang.org/x/net v0.18.0
# golang.org/x/net v0.24.0
## explicit; go 1.18
golang.org/x/net/publicsuffix
# golang.org/x/oauth2 v0.14.0
## explicit; go 1.18
golang.org/x/oauth2
golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/internal
# google.golang.org/appengine v1.6.8
## explicit; go 1.11
Expand All @@ -71,13 +72,15 @@ google.golang.org/appengine/internal/log
google.golang.org/appengine/internal/remote_api
google.golang.org/appengine/internal/urlfetch
google.golang.org/appengine/urlfetch
# google.golang.org/protobuf v1.31.0
## explicit; go 1.11
# google.golang.org/protobuf v1.34.0
## explicit; go 1.17
google.golang.org/protobuf/encoding/prototext
google.golang.org/protobuf/encoding/protowire
google.golang.org/protobuf/internal/descfmt
google.golang.org/protobuf/internal/descopts
google.golang.org/protobuf/internal/detrand
google.golang.org/protobuf/internal/editiondefaults
google.golang.org/protobuf/internal/editionssupport
google.golang.org/protobuf/internal/encoding/defval
google.golang.org/protobuf/internal/encoding/messageset
google.golang.org/protobuf/internal/encoding/tag
Expand All @@ -100,6 +103,7 @@ google.golang.org/protobuf/reflect/protoregistry
google.golang.org/protobuf/runtime/protoiface
google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/types/descriptorpb
google.golang.org/protobuf/types/gofeaturespb
# gopkg.in/h2non/gentleman.v2 v2.0.5
## explicit
gopkg.in/h2non/gentleman.v2
Expand Down

0 comments on commit 0ced760

Please sign in to comment.